经典排序算法总结与Python实现(下)
在之前的博客经典排序算法总结与Python实现(上)中已经讨论过插入、冒泡、选择、快排、谢尔排序。这篇博客主要完成剩下的几个排序算法。
排序算法 | 时间复杂度(最好) | 时间复杂度(最差) | 时间复杂度(平均) | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
归并排序 | O(nlogn)O(n\log{n})O(nlogn) | O(nlogn)O(n\log{n})O(nlogn) | O(nlogn)O(n\log{n})O(nlogn) | O(n)O(n)O(n) | 稳定 |
堆排序 | O(nlogn)O(n\log{n})O(nlogn) | O(nlogn)O(n\log{n})O(nlogn) | O(nlogn)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+nlognk)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(logn)O(\log n)O(logn),合并和过程中数组每个元素都会被比较,所以是O(n)O(n)O(n)复杂度,综合来看,归并排序的时间复杂度为O(nlogn)O(n\log n)O(nlogn)
稳定性:不会改变原本大小相等的元素的相对位置,是稳定的。
空间复杂度:需要一个额外的数组空间来存放合并后的新数组,因此空间复杂度为O(n)O(n)O(n)
2. 堆排序
堆排序利用了堆这种数据结构,最大堆是指所有父结点的值大于等于左右子结点的一种完全二叉树(最小堆则是小于等于),因此最大堆的根结点是数组中的最大值,利用这种结构,堆排序是将无序数组构建成一个最大堆,然后将根结点与最后一个元素交换,然后再对前n-1个元素再构建最大堆,再把根结点与倒数第二个元素交换,这样直到只剩最后一个元素的堆自然满足最大堆的性质,它也是数组中的最小元素,整个数组也就有序了。
总的来说,堆排序可以分为三步(重复第2、3步直到size为1):
- 对数组构建最大堆
- 交换最大元素
- 对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(logn)O(\log n)O(logn)复杂度,因为n个结点的完全二叉树的深度为⌈log2(n+1)⌉\lceil \log_2 (n+1)\rceil⌈log2(n+1)⌉,所以是O(n)+n∗O(logn)O(n)+n*O(\log n)O(n)+n∗O(logn)复杂度,综合来看,归并排序的时间复杂度为O(nlogn)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(nlogn)O(n\log n)O(nlogn),一共有k个桶,每个桶平均有n/kn/kn/k个元素,所以桶的排序需要O(k×nklognk)=O(nlognk)O(k\times {n\over k}\log {n\over k})=O(n\log {n\over k})O(k×knlogkn)=O(nlogkn),因此总的为O(n+nlognk)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实现(下)相关推荐
- 十大经典排序算法6(Python版本)
文章目录 九.桶排序 十.基数排序 九.桶排序 1.桶排序介绍 桶排序是计数排序的升级版.它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定.为了使桶排序更加高效,我们需要做到这两点: 在 ...
- 十大经典排序算法4(Python版本)
文章目录 六.快速排序 六.快速排序 1.快速介绍 快速排序是由东尼·霍尔所发展的一种排序算法.在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较.在最坏状况下则需要 Ο(n2) 次比较,但这 ...
- 十大经典排序算法3(Python版本)
文章目录 四.希尔排序 五.归并排序 四.希尔排序 1.希尔介绍 希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本.但希尔排序是非稳定排序算法. 希尔排序是基于插入排序的以下两点性质而 ...
- 经典排序算法总结与Python实现(上)
本篇博客中的有序都是指的升序,降序情况稍微改改就出来了. 排序算法 时间复杂度(最好) 时间复杂度(最差) 时间复杂度(平均) 空间复杂度 稳定性 插入排序 O(n)O(n)O(n) O(n2)O(n ...
- 十大经典排序算法5(Python版本)
文章目录 七.堆排序 八.计数排序 七.堆排序 1.堆排序介绍 堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法.堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键 ...
- 十大经典排序算法2(Python版本)
文章目录 二.选择排序 三.插入排序 二.选择排序 1.选择介绍 选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度.所以用到它的时候,数据规模越小越好.唯一的好处可能就 ...
- 十大经典排序算法1(Python版本)
文章目录 一.排序算法与时间复杂度 二.冒泡排序 一.排序算法与时间复杂度 1.十大排序算法 排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次 ...
- python遍历数组冒泡排序_经典排序算法(冒泡排序,选择排序,插入排序,快速排序,堆排序)python实现...
最近在复习经典排序算法,自己用python也实现了一下,这里不会涉及到原理(因为网上方法已经很详细啦),就把函数贴上来,可以让大家自己试着运行下,再结合别处的原理也可以更好地理解它们的实现. 如果有错 ...
- python经典排序_python实现十大经典排序算法
写在前面 本文参考十大经典排序算法(动图演示),这篇文章有动图显示,介绍的很详细.本文是部分内容有借鉴此博客,用python实现,有一些改进. 各种算法的时间.空间复杂度 1.冒泡排序 1.比较相邻的 ...
最新文章
- ERROR: Manifest merger failed : uses-sdk:minSdkVersion 24 cannot be smaller than version 27 declared
- 图像分段线性变化_暗光也清晰的图像增强算法
- Three.js中使用requestAnimationFrame方法实现立方体转动和小球跳动的动画
- ubuntu设置自启动服务程序
- visual studio 正则表达式 查找与替换文本
- Linux---进程调度相关命令解析
- 赛门铁克运维注意事项
- HTML五合一收款码网站源码(带35套模板)
- 类的扩充 js中面向对象的技术
- mxnet 训练--如何生成rec 数据 +自己在本机测试的结果
- ubuntu上 grafana + influxdb + telegraf 安装配置
- scp(安全副本)到ec2实例,无需密码
- Conda 环境常用碎笔记
- 无监督︱异常、离群点检测 一分类——OneClassSVM
- Google 周三宣布新版Google Trend上线
- sci的figure怎么做_SCI论文中Figure图如何制作?
- 测试开发大厂面试精选40题
- 分布式机器学习主要笔记
- 遗传算法求解多元函数极值点-C++实现
- 共享文件问题 -- 无法访问 您可能没有权限使用网络资源