算法:排序精讲

所有非比较类排序算法(计数排序、桶排序、基数排序)都是稳定的,比较类排序算法中,除了冒泡排序、简单插入、归并排序这三种算法是稳定的,其他算法都是不稳定的。
参考链接
本文内容整理自以下几篇博客: 十大经典排序算法(动图演示) 十大经典排序算法动画,看我就够了! 视频 | 手撕九大经典排序算法,看我就够了! 排序算法(九):桶排序
排序算法分类

算法稳定性
所谓稳定性,是指在排序的过程中,元素原来的相对次序是否变化,稳定的排序算法不会改变原始的相对次序,而不稳定的排序算法则会改变这个次序。

算法名称 | 稳定性 | 时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 |
---|---|---|---|---|---|
冒泡排序 | 稳定 | \(O(n^2)\) | \(O(n^2)\) | \(O(n)\) | \(O(1)\) |
快速排序 | 不稳定 | \(O(nlog_2n)\) | \(O(n^2)\) | \(O(nlog_2n)\) | \(O(nlog_2n)\) |
简单插入排序 | 稳定 | \(O(n^2)\) | \(n(n^2)\) | \(O(n)\) | \(O(1)\) |
希尔排序 | 不稳定 | \(O(n^{\frac{3}{2}})\) | \(O(n^2)\) | \(O(n)\) | \(O(1)\) |
简单选择排序 | 不稳定 | \(O(n^2)\) | \(n(n^2)\) | \(O(n^2)\) | \(O(1)\) |
堆排序 | 不稳定 | \(O(nlog_2n)\) | \(n(nlog_2n)\) | \(O(nlog_2n)\) | \(O(1)\) |
二路归并排序 | 稳定 | \(O(nlog_2n)\) | \(O(nlog_2n)\) | \(O(nlog_2n)\) | \(O(n)\) |
计数排序 | 稳定 | \(O(n + k)\) | \(O(n + k)\) | \(O(n + k)\) | \(O(n + k)\) |
桶排序 | 稳定 | \(O(n+k)\) | \(n(n^2)\) | \(O(n)\) | \(O(n+k)\) |
基数排序 | 稳定 | \(O(n \times k)\) | \(O(n \times k)\) | \(O(n \times k)\) | \(O(n + k)\) |
下列排序方法中,排序所花费时间不受数据初始排列特性影响的算法是(C)。 A.直接插入排序 B.冒泡排序 C.直接选择排序 D.快速排序
下列排序算法中时间复杂度不受数据初始状态影响,恒为 O(\(n^2\)) 的是(C) A. 堆排序 B. 冒泡排序 C. 直接选择排序 D. 快速排序
外部排序和内部排序
- 外部排序指的是大文件的排序,即待排序的记录存储在外存储器上,待排序的文件无法一次装入内存,需要在内存和外部存储器之间进行多次数据交换,以达到排序整个文件的目的。
- 内部排序是指待排序列完全存放在内存中所进行的排序过程,适合不太大的元素序列。
比较类排序
通过元素比较来决定相对次序,时间复杂度不能突破 \(O(nlogn)\),又称非线性时间比较类排序。
交换排序
冒泡排序(Bubble Sort)
算法思想
重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换的元素,也就是说该数列已经排序完成。
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- 针对所有的元素重复以上的步骤,每次都从第0个元素开始两两交换,第
n
次交换到第len(nums)-1-n
个元素,0<n<len(nums)-1
; - 重复步骤1~3,直到排序完成。
图示

Python实现
1 |
|
时空复杂度和稳定性
稳定性 | 时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 |
---|---|---|---|---|
稳定 | \(O(n^2)\) | \(O(n^2)\) | \(O(n)\) | \(O(1)\) |
快速排序(Quick Sort)
算法思想
基于分治的思想
- 先从数列中取出一个数作为key值;
- 将比这个数小的数全部放在它的左边,大于或等于它的数全部放在它的右边;
- 递归的对左右两个小数列重复第二步,直至各区间只有1个数。
图示
Python实现
- 推荐方法
1 |
|
- 另一种方法(这种算法在某些时候会陷入死循环)详见剑指offer 面试题45. 把数组排成最小的数(中)。
1 |
|
时空复杂度和稳定性
稳定性 | 时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 |
---|---|---|---|---|
不稳定 | \(O(nlog_2n)\) | \(O(n^2)\) | \(O(nlog_2n)\) | \(O(nlog_2n)\) |
插入排序
简单插入排序(Insertion Sort)
算法思想
将无序部分中的元素插入到有序部分。初始化时,从第一个元素开始,依次搜索每一个元素的待插入位置,算法中,有序部分在前,无序部分在后,一边寻找插入位置,一边将元素后移一个单元。一直在往前找适和当前元素的位置。
图示
Python实现
1 |
|
时空复杂度和稳定性
稳定性 | 时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 |
---|---|---|---|---|
稳定 | \(O(n^2)\) | \(n(n^2)\) | \(O(n)\) | \(O(1)\) |
希尔排序(Shell Sort)
算法思想
简单插入排序的修改版,根据步长由长到短分组,进行排序,直到步长为1为止,属于插入排序的一种。1959年Shell发明,第一个突破 \(O(n^2)\) 的排序算法,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
图示

Python实现
1 |
|
时空复杂度和稳定性
希尔排序的平均时间复杂度和步长序列有之间关系
稳定性 | 步长序列 | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 |
---|---|---|---|---|
不稳定 | \(2^i3^j\) | \(O(nlog^2n)\) | \(O(n)\) | \(O(n)\) |
\(2^k-1\) | \(O(n^{\frac{3}{2}})\) | \(O(n)\) | \(O(n)\) | |
\(\frac{n}{2^i}\) | \(O(n^2)\) | \(O(n)\) | \(O(n)\) |
选择排序
简单选择排序(Selection Sort)
算法思想
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始(末尾)位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到未排序序列的末尾。以此类推,直到所有元素均排序完毕。注意,这里有个有序区和无序区的概念。
图示
Python实现
1 |
|
时空复杂度和稳定性
稳定性 | 时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 |
---|---|---|---|---|
不稳定 | \(O(n^2)\) | \(n(n^2)\) | \(O(n^2)\) | \(O(1)\) |
堆排序(Heap Sort)
算法思想
堆排序是一种基于二叉堆(Binary Heap)结构的排序算法,所谓二叉堆,就是一种特殊的完全二叉树,只不过相比较完全二叉树而言,二叉堆的所有父节点的值都大于(或者小于)它的孩子节点,像这样:

首先需要引入最大堆(大根堆)的定义:
- 最大堆中的最大元素值出现在根结点(堆顶)
- 堆中每个父节点的元素值都大于等于其孩子结点
类似的,还有最小堆(小根堆):
- 最大堆中的最小元素值出现在根结点(堆顶)
- 堆中每个父节点的元素值都小于等于其孩子结点

那么,堆排序算法的内容就很简单了,最大堆堆顶的最大数取出,将剩余的堆继续调整为最大堆,再次将堆顶的最大数取出,这个过程持续到剩余数只有一个时结束。
图示
Python实现
1 |
|
时空复杂度和稳定性
稳定性 | 时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 |
---|---|---|---|---|
不稳定 | \(O(nlog_2n)\) | \(n(nlog_2n)\) | \(O(nlog_2n)\) | \(O(1)\) |
归并排序
二路归并排序(Merge Sort)
算法思想
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)思想。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。若将K个有序表合并成一个有序表,称为多路归并排序。
- 如果给的数组只有一个元素的话,直接返回(也就是递归到最底层的情况)
- 把整个数组分为尽可能相等的两个部分(分)
- 对于两个被分开的两个部分进行整个归并排序(治)
- 把两个被分开且排好序的数组拼接在一起
图示

Python实现
1 |
|
时空复杂度和稳定性
稳定性 | 时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 |
---|---|---|---|---|
稳定 | \(O(nlog_2n)\) | \(O(nlog_2n)\) | \(O(nlog_2n)\) | \(O(n)\) |
非比较类排序
不通过比较来决定元素间的相对次序,时间复杂度可以达到 \(O(n)\),又称线性时间非比较类排序。
计数排序(Counting Sort)
算法思想
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。条件苛刻,且牺牲空间换取时间。
图示
Python实现
1 |
|
时空复杂度和稳定性
稳定性 | 时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 |
---|---|---|---|---|
稳定 | \(O(n + k)\) | \(O(n + k)\) | \(O(n + k)\) | \(O(n + k)\) |
\(n\) 是待排序元素个数,\(k\) 是待排序元素区间长度。
桶排序(Bucket sort)
算法思想
桶排序是计数排序的升级版,桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
桶排序过程中存在两个关键环节: 1、元素值域的划分,也就是元素到桶的映射规则。映射规则需要根据待排序集合的元素分布特性进行选择。 2、排序算法的选择,从待排序集合中元素映射到各个桶上的过程,并不存在元素的比较和交换操作,在对各个桶中元素进行排序时,可以自主选择合适的排序算法,桶排序算法的复杂度和稳定性,都根据选择的排序算法不同而不同。
算法过程:
- 设置一个定量的数组当作空桶;
- 遍历输入数据,并且把数据一个一个放到对应的桶里去;
- 对每个不是空的桶进行排序;
- 从不是空的桶里把排好序的数据拼接起来。
图示

Python实现
1 |
|
时空复杂度和稳定性
稳定性 | 时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 |
---|---|---|---|---|
稳定 | \(O(n+k)\) | \(n(n^2)\) | \(O(n)\) | \(O(n+k)\) |
\(n\) 是待排序元素个数,\(k\) 是待排序元素区间长度。
基数排序(Radix Sort)
算法思想
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
- 取得数组中的最大数,并取得位数;
nums
为原始数组,从最低位开始取每个位组成radix
数组;- 对
radix
进行计数排序(利用计数排序适用于小范围数的特点);
图示

Python实现
1 |
|
时空复杂度和稳定性
稳定性 | 时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 |
---|---|---|---|---|
稳定 | \(O(n \times k)\) | \(O(n \times k)\) | \(O(n \times k)\) | \(O(n + k)\) |
其中 \(n\) 是排序元素个数,\(k\) 是数字位数。
Python有点意思
两个 Python list
的内建函数
1 |
|
i
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!