在之前的博客经典排序算法总结与Python实现(上)中已经讨论过插入、冒泡、选择、快排、谢尔排序。这篇博客主要完成剩下的几个排序算法。

排序算法 时间复杂度(最好) 时间复杂度(最差) 时间复杂度(平均) 空间复杂度 稳定性
归并排序 O(nlog⁡n)O(n\log{n})O(nlogn) O(nlog⁡n)O(n\log{n})O(nlogn) O(nlog⁡n)O(n\log{n})O(nlogn) O(n)O(n)O(n) 稳定
堆排序 O(nlog⁡n)O(n\log{n})O(nlogn) O(nlog⁡n)O(n\log{n})O(nlogn) O(nlog⁡n)O(n\log{n})O(nlogn) O(1)O(1)O(1) 不稳定
计数排序 O(n+k)O(n+k)O(n+k) O(n+k)O(n+k)O(n+k) O(n+k)O(n+k)O(n+k) O(n+k)O(n+k)O(n+k) 稳定
桶排序 O(n)O(n)O(n) O(n2)O(n^2)O(n2) O(n+nlog⁡nk)O(n+n\log{n\over k})O(n+nlogkn​) O(n+k)O(n+k)O(n+k) 稳定
基数排序 O(d∗(n+k))O(d*(n+k))O(d∗(n+k)) O(d∗(n+k))O(d*(n+k))O(d∗(n+k)) O(d∗(n+k))O(d*(n+k))O(d∗(n+k)) O(n+k)O(n+k)O(n+k) 稳定

目录

  • 1. 归并排序
  • 2. 堆排序
  • 3. 计数排序
  • 4. 桶排序
  • 5. 基数排序
  • 参考资料

1. 归并排序

归并排序采用分治的思想,将数组等分为左右两个子数组,分别对子数组进行归并排序,然后对排序后的有序子数组进行组合。组合的方式就是每次对比两个子数组的最小值(最左边的元素),那个小就取哪个,这样某个子数组就少了一个元素,下一次也是如此对比当前子数组的最小值,最后直到某个子数组被取空了,那直接在新合并后的数组后面将剩下的那个子数组extend上去即可。

def mergeSort(a):# 结束条件if len(a) <= 1:return a# 分治mid = len(a) // 2left = mergeSort(a[:mid])right = mergeSort(a[mid:])# 合并merged = []while left and right:if left[0] <= right[0]:merged.append(left.pop(0))else:merged.append(right.pop(0))merged.extend(right if right else left)return merged

性能分析

  • 时间复杂度:不管是最好的情况(数组已经有序)还是其他情况,归并排序都是需要分治和合并两个步骤,分治的复杂度为O(log⁡n)O(\log n)O(logn),合并和过程中数组每个元素都会被比较,所以是O(n)O(n)O(n)复杂度,综合来看,归并排序的时间复杂度为O(nlog⁡n)O(n\log n)O(nlogn)

  • 稳定性:不会改变原本大小相等的元素的相对位置,是稳定的。

  • 空间复杂度:需要一个额外的数组空间来存放合并后的新数组,因此空间复杂度为O(n)O(n)O(n)

2. 堆排序

堆排序利用了堆这种数据结构,最大堆是指所有父结点的值大于等于左右子结点的一种完全二叉树(最小堆则是小于等于),因此最大堆的根结点是数组中的最大值,利用这种结构,堆排序是将无序数组构建成一个最大堆,然后将根结点与最后一个元素交换,然后再对前n-1个元素再构建最大堆,再把根结点与倒数第二个元素交换,这样直到只剩最后一个元素的堆自然满足最大堆的性质,它也是数组中的最小元素,整个数组也就有序了。

总的来说,堆排序可以分为三步(重复第2、3步直到size为1):

  1. 对数组构建最大堆
  2. 交换最大元素
  3. 对size-1的数组调整最大堆

堆排序的关键在于如何构建最大堆(若要数组降序排列则建立最小堆)。因为叶子结点没有孩子,已经算是一个合法的堆,所以只需要从最后一个非叶子结点开始调整。而因为堆是完全二叉树,最后一个非叶子结点的坐标为n//2−1n//2-1n//2−1(详细推导在下面),将这个节点作为堆顶构造最大堆,然后再对倒数第二个非叶子结点构造最大堆,这样从最后一个非叶子结点开始,依次构造最大堆,直到最后根结点(坐标为0)为堆顶的最大堆构造完成。每次在对新的非叶子结点进行构造时,若该节点已经比其左右孩子都大,那就不需要调整,因为其左右孩子已经是最大堆了;否则,选择左右孩子中较大的值与该节点交换,再递归地对交换的那个子树进行调整。构建最大堆和调整最大堆的代码如下:

def buildHeap(a):for i in range(len(a)//2-1, -1, -1):# 从最后一个非叶子结点开始,逆序地对所有非叶子结点调整堆结构adjustHeap(a, i, len(a))    # 对当前节点位置i,其左右孩子都是最大堆,以长度为n的数组调整最大堆
def adjustHeap(a, i, n):root = i # 当前需要调整的非叶子结点while True:# 选择较大的子节点坐标max_childmax_child = 2 * root + 1  # 先初始化为左孩子if max_child >= n:breakif max_child + 1 < n and a[max_child+1] > a[max_child]:max_child += 1  # 与右孩子比较# 比较,看需不需要调整if a[max_child] > a[root]:a[max_child], a[root] = a[root], a[max_child]root = max_child # 对交换的那个子树再进行调整else:break  

建立好最大堆之后对最大堆的堆顶元素和最后一个元素进行交换。再需将交换后的堆顶元素进行调整,建立size-1的最大堆再进行交换,这样依次减小需要调整的size,就可以完成堆排序了,即:

def heapSort(a):# 对数组构建最大堆buildHeap(a)# 缩小sizefor size in range(len(a), 1, -1):# 交换最大元素a[0], a[size-1] = a[size-1], a[0]# 对size-1的数组调整最大堆adjustHeap(a, 0, size-1)return a
  • 最后一个非叶子结点的坐标的推导(参考《数据结构、算法与应用C++》):
    对于完全二叉树的节点i(0≤i≤n−1)i\ (0\le i\le n-1)i (0≤i≤n−1),其左孩子的坐标为2i+12i+12i+1,其右孩子的坐标为2i+22i+22i+2
    最后一个非叶子结点的坐标为n−1n-1n−1,当完全二叉树的节点数n为偶数时,最后一个非叶子结点只有左孩子,所以2i+1=n−12i+1=n-12i+1=n−1,其父结点坐标i=(n−2)/2=n/2−1=n//2−1i=(n-2)/2=n/2-1=n//2-1i=(n−2)/2=n/2−1=n//2−1;当n为奇数时,最后一个非叶子结点还有右孩子,所以2i+2=n−12i+2=n-12i+2=n−1,其父结点坐标i=(n−1−2)/2=(n−1)/2−1=n//2−1i=(n-1-2)/2=(n-1)/2-1=n//2-1i=(n−1−2)/2=(n−1)/2−1=n//2−1。综上得证。

性能分析

  • 时间复杂度:从步骤上来看,第一步需要对整个数组进行建堆,建堆的复杂度为O(n)O(n)O(n),第二步和第三步需要重复n-1次,每次第二步交换是O(1)O(1)O(1)复杂度,第三步的调整堆是O(log⁡n)O(\log n)O(logn)复杂度,因为n个结点的完全二叉树的深度为⌈log⁡2(n+1)⌉\lceil \log_2 (n+1)\rceil⌈log2​(n+1)⌉,所以是O(n)+n∗O(log⁡n)O(n)+n*O(\log n)O(n)+n∗O(logn)复杂度,综合来看,归并排序的时间复杂度为O(nlog⁡n)O(n\log n)O(nlogn)

  • 稳定性:不稳定。

  • 空间复杂度:在比较和交换时需要一个额外的数组空间,因此空间复杂度为O(1)O(1)O(1)

3. 计数排序

对于数据范围比较小,数据量比较多的数组进行排序,采用计数排序会大大节省时间。因为计数排序开辟了较大的空间来计数每个元素出现的次数,假设数组每个元素都在0到k之间,则对数组中的每个元素进行遍历,大小为i则对count[i]加1,然后再根据count的数量来反向填充数组。

def countSort(a):# 数组的最大值k = max(a)# 用于计数的数组count = [0] * (k+1)ans = []# 计数for i in a:count[i] += 1# 填充ansfor j in range(k+1):while count[j] > 0:ans.append(j)count[j] -= 1 # 填充了一个就减1return ans

性能分析

  • 时间复杂度:计数需要遍历一次数组,为O(n)O( n)O(n)复杂度,填充ans需要遍历计数数组,为O(k)O( k)O(k)复杂度,所以总共的复杂度为O(n+k)O(n+k)O(n+k)

  • 稳定性:认为先计数的先append,所以是稳定的。

  • 空间复杂度:计数需要O(k)O( k)O(k)复杂度的空间,每个count内需要O(n)O( n)O(n)复杂度的空间,所以总共的空间复杂度为O(n+k)O(n+k)O(n+k)

4. 桶排序

桶排序类似于计数排序,区别在于计数排序是每个数值计数一次,而桶排序的每个桶表示一个区间,落在这个区间中的数就直接映射到这个桶中,然后再分别对每个桶进行排序(可使用其他比较排序的方法,例如插入排序),最后将所有不为空的桶拼接起来就得到了有序的数组。

假设设定k个桶,按照这个思路算法实现如下:

# 自己默认设置5个桶
def bucketSort(a, k=5):min_value = min(a)max_value = max(a)ans = []# 桶间距distance = (max_value - min_value) / (k - 1)# 初始化桶bucket = [[] for _ in range(k)]# 遍历数据放入桶内for i in a:bucket[int((i - min_value) / distance)].append(i)# 每个桶排序并拼接for j in range(k):bucket[j].sort() # 这里直接调用sort函数ans.extend(bucket[j])return ans

性能分析

  • 最好情况:数组已经是排好序的情况下,遍历一次数据放入桶内和拼接都为O(n)O(n)O(n)复杂度,只需要看每个桶内的排序复杂度,最好情况下为O(n)O(n)O(n),因此时间复杂度为O(n)O(n)O(n)

  • 最坏情况:使用插入或者快排等每个桶内最坏情况下的排序复杂度为O(n2)O(n^2)O(n2),而其余步骤只要O(n)O(n)O(n),因此时间复杂度为O(n2)O(n^2)O(n2)

  • 平均情况:每个桶内平均的排序复杂度为O(nlog⁡n)O(n\log n)O(nlogn),一共有k个桶,每个桶平均有n/kn/kn/k个元素,所以桶的排序需要O(k×nklog⁡nk)=O(nlog⁡nk)O(k\times {n\over k}\log {n\over k})=O(n\log {n\over k})O(k×kn​logkn​)=O(nlogkn​),因此总的为O(n+nlog⁡nk)O(n+n\log{n\over k})O(n+nlogkn​)

  • 稳定性:稳定性取决于桶内的排序算法,可以做到稳定

  • 空间复杂度:需要O(k)O( k)O(k)复杂度的桶空间,桶内最多需要O(n)O( n)O(n)复杂度的空间,所以总共的空间复杂度为O(n+k)O(n+k)O(n+k)

5. 基数排序

基数排序可以分为最高位优先(Most Significant Digit first)法和最低位优先(Least Significant Digit first)法,LSD是从最低位也就是个位开始排序(类似计数排序的方式),然后再按照十位排序,直到数组中最高位排序后整个数组就有序了。

import math# 默认基数k为10
def radixSort(a, k=10):# 最高位数d = math.ceil(math.log(max(a), k))# 从最低位开始排序for i in range(d):bucket = [[] for _ in range(k)]# 比较第i位放入相应的桶内for num in a:bucket[num % (k**(i+1)) // (k**i)].append(num)a.clear()# 拼接桶重新赋值a,为下一次排序做准备for b in bucket:a.extend(b)return a

性能分析

  • 时间复杂度:一次排序分入桶内需要O(n)O(n)O(n),拼接需要O(k)O(k)O(k),一共进行O(d)O(d)O(d)次排序,所以总的时间复杂度为O(d∗(n+k))O(d*(n+k))O(d∗(n+k)),kkk为基数,也就是桶的数量,ddd为最大数的位数

  • 稳定性:每次排序都是稳定,所以总的来说也是稳定的

  • 空间复杂度:需要O(k)O(k)O(k)复杂度的桶空间,桶内最多需要O(n)O( n)O(n)复杂度的空间,所以总共的空间复杂度为O(n+k)O(n+k)O(n+k)

参考资料

1、数据结构、算法与应用 C++语言描述 原书第2版
2、MOOC:数据结构与算法Python版(北大 陈斌老师)
3、参考博客1:十大经典排序算法(动图演示)
4、参考博客2:排序算法总结(Python版)
5、百度百科

经典排序算法总结与Python实现(下)相关推荐

  1. 十大经典排序算法6(Python版本)

    文章目录 九.桶排序 十.基数排序 九.桶排序 1.桶排序介绍 桶排序是计数排序的升级版.它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定.为了使桶排序更加高效,我们需要做到这两点: 在 ...

  2. 十大经典排序算法4(Python版本)

    文章目录 六.快速排序 六.快速排序 1.快速介绍 快速排序是由东尼·霍尔所发展的一种排序算法.在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较.在最坏状况下则需要 Ο(n2) 次比较,但这 ...

  3. 十大经典排序算法3(Python版本)

    文章目录 四.希尔排序 五.归并排序 四.希尔排序 1.希尔介绍 希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本.但希尔排序是非稳定排序算法. 希尔排序是基于插入排序的以下两点性质而 ...

  4. 经典排序算法总结与Python实现(上)

    本篇博客中的有序都是指的升序,降序情况稍微改改就出来了. 排序算法 时间复杂度(最好) 时间复杂度(最差) 时间复杂度(平均) 空间复杂度 稳定性 插入排序 O(n)O(n)O(n) O(n2)O(n ...

  5. 十大经典排序算法5(Python版本)

    文章目录 七.堆排序 八.计数排序 七.堆排序 1.堆排序介绍 堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法.堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键 ...

  6. 十大经典排序算法2(Python版本)

    文章目录 二.选择排序 三.插入排序 二.选择排序 1.选择介绍 选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度.所以用到它的时候,数据规模越小越好.唯一的好处可能就 ...

  7. 十大经典排序算法1(Python版本)

    文章目录 一.排序算法与时间复杂度 二.冒泡排序 一.排序算法与时间复杂度 1.十大排序算法 排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次 ...

  8. python遍历数组冒泡排序_经典排序算法(冒泡排序,选择排序,插入排序,快速排序,堆排序)python实现...

    最近在复习经典排序算法,自己用python也实现了一下,这里不会涉及到原理(因为网上方法已经很详细啦),就把函数贴上来,可以让大家自己试着运行下,再结合别处的原理也可以更好地理解它们的实现. 如果有错 ...

  9. python经典排序_python实现十大经典排序算法

    写在前面 本文参考十大经典排序算法(动图演示),这篇文章有动图显示,介绍的很详细.本文是部分内容有借鉴此博客,用python实现,有一些改进. 各种算法的时间.空间复杂度 1.冒泡排序 1.比较相邻的 ...

最新文章

  1. ERROR: Manifest merger failed : uses-sdk:minSdkVersion 24 cannot be smaller than version 27 declared
  2. 图像分段线性变化_暗光也清晰的图像增强算法
  3. Three.js中使用requestAnimationFrame方法实现立方体转动和小球跳动的动画
  4. ubuntu设置自启动服务程序
  5. visual studio 正则表达式 查找与替换文本
  6. Linux---进程调度相关命令解析
  7. 赛门铁克运维注意事项
  8. HTML五合一收款码网站源码(带35套模板)
  9. 类的扩充 js中面向对象的技术
  10. mxnet 训练--如何生成rec 数据 +自己在本机测试的结果
  11. ubuntu上 grafana + influxdb + telegraf 安装配置
  12. scp(安全副本)到ec2实例,无需密码
  13. Conda 环境常用碎笔记
  14. 无监督︱异常、离群点检测 一分类——OneClassSVM
  15. Google 周三宣布新版Google Trend上线
  16. sci的figure怎么做_SCI论文中Figure图如何制作?
  17. 测试开发大厂面试精选40题
  18. 分布式机器学习主要笔记
  19. 遗传算法求解多元函数极值点-C++实现
  20. 共享文件问题 -- 无法访问 您可能没有权限使用网络资源

热门文章

  1. Win10 打开PLSql,其他软件卡到爆,原因CnPlugin_1.5.4插件的问题
  2. 关于get请求的乱码问题
  3. 华中农业大学第五届程序设计大赛网络同步赛解题报告2(转)
  4. 查看mysql的安装信息
  5. VC菜菜鸟-创建一个简单的多线程任务
  6. 物联网系统开发如何选择时序数据库
  7. 第一章:了解SQL_数据库基础
  8. 模板 manacher算法
  9. 重写方法,重载方法,虚方法和抽象方法的使用
  10. 杂项:Java un