常用的排序方法
-
排序算法的
稳定性
是指:在需要进行排序操作的数据中,如果存在值相等的元素,在排序前后,相等元素之间的排列顺序不发生改变
。 -
字母
n
表示排序的数组或链表的元素个数
。 -
排序的对象是:数组或链表
。
比较
最好时间复杂度 | 最坏时间复杂度 | 平均时间复杂度 | 空间复杂度 | 排序方式 | 稳定性 | 排序对象 | 备注 | |
---|---|---|---|---|---|---|---|---|
冒泡排序(Bubble Sort) | O(n) | O(n2) | O(n2) | O(1) | In-Place | 稳定 | 数组 | |
直接插入排序(Insertion Sort) | O(n) | O(n2) | O(n2) | O(1) | In-Place | 稳定 | 数组、链表 | |
归并排序(Merge Sort) | O(n log n) | O(n log n) | O(n log n) | O(n) | Out-Place | 稳定 | 数组、链表 | |
计数排序(Count Sort) | O(n + k) | O(n2) | O(n + k) | O(k) | Out-Place | 稳定 | 数组、链表 | 待排序的数据必须是有确定范围的整数 |
桶排序(Bucket Sort) | O(n + k) | O(n2) | O(n + k) | O(n + k) | Out-Place | 稳定 | 数组、链表 | 待排序的数据必须是有确定范围的整数、计数排序的升级版 |
基数排序(Radix Sort) | O(n * k) | O(n * k) | O(n * k) | O(n + k) | Out-Place | 稳定 | 数组、链表 | 待排序的数据必须是有确定范围的整数 |
选择排序(Selection Sort) | O(n2) | O(n2) | O(n2) | O(1) | In-Place | 不稳定 | 数组、链表 | |
堆排序(Heap Sort) | O(n log n) | O(n log n) | O(n log n) | O(1) | In-Place | 不稳定 | 数组 | |
快速排序(Quick Sort) | O(n log n) | O(n2) | O(n log n) | O(n log n) | In-Place | 不稳定 | 数组 | |
希尔排序(Shell’s Sort) | O(n log2 n) | O(n log2 n) | O(n log n) | O(1) | In-Place | 不稳定 | 数组 | 直接插入排序的升级版、适合初始数据偏向有序的数据 |
稳定的排序算法
冒泡排序(Bubble Sort)
排序方式 和 对象
In-Place
(在正确的位置),数据对象是:数组
。
时间复杂度
-
平均
:O(n2) -
最好
:O(n),此时,第一次排序后,元素全部归位
,第二次比较时确认一下即可跳出循环。 -
最坏
:O(n2),比较到最后才把全部元素归位
。
空间复杂度
- O(1),
只需要添加一个元素用来交换数组元素
,与数组大小无关。
原理
-
比较相邻的元素,如果前者大于(小于)后者,就交换两者
。 -
从第一对(0 和 1)到最后一对(n-2 和 n-1),
重复比较
;交换完到最后一对时,最后一个元素应该是最大(最小)的数
。 -
剔除上一次循环归位的元素后,重复以上的步骤
,继续循环。 -
直到
剩余数据只有一位
;此时排序完成。或者,在当前循环中检测到所有元素都已归位
,此时结束当前循环后,排序结束
。 -
注意:定义一个布尔变量 hasChange,用来
标记每轮是否进行了交换
,不交换时,数组所有元素都已归位。可以结束排序了。 -
在每轮遍历开始时,将 hasChange 设置为 false;若当轮没有发生交换(hasChange == false),说明此时
数组已经按照升序(降序)排列,可以直接结束排序了
。
插入排序(直接插入排序,Insertion Sort)
排序方式
In-Place
(在正确的位置),数据对象是:数组
和链表
。
时间复杂度
-
平均
:O(n2) -
最好
:O(n) -
最坏
:O(n2)
空间复杂度
- O(1),
只需要添加一个元素用来交换数组(链表)元素
,与数组(链表)大小无关。
原理
-
插入算法的
核心思想是:取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入
。 -
我们将数组(链表)中的数据分为两个区间:
已排序区间
和未排序区间
。初始已排序区间只有一个元素,就是数组的第一个元素
。 -
插入排序
每次会从未排序区间中找一个元素(这个元素一般是去未排序区第一位元素),在已排序区间中找到合适的插入位置将其插入
,保证已排序区间的数据一直有序。 -
重复这个过程,
直到未排序区间中元素为空
,算法结束。 -
注意:无论怎样都
无法提前结束的排序算法的
。
归并排序(Merge Sort)
排序方式
Out-Place
(在不正确的位置),数据对象是:数组
和链表
。
时间复杂度
-
平均
:O(n log n) -
最好
:O(n log n) -
最坏
:O(n log n)
空间复杂度
- O(n) 或 O(n) + O(log n)
原理
-
归并排序的核心思想是
分治:把一个复杂问题拆分成若干个子问题来求解
。 -
把数组(链表)从中间划分为两个子数组(子链表)。
-
一直递归地把子数组(子链表)划分成更小的数组(子链表)
,直到子数组(子链表)是完全有序的数组(链表)
。(一般数组或链表里面只有一个元素的时候是可以保证是完全有序的) -
归并数组(链表)时:按照大小顺序合并两个子数组(子链表)。
接着依次按照递归的顺序返回,不断合并排好序的数组(链表)
,直到把整个数组(链表)排好序。
计数排序(Count Sort)
排序方式
-
Out-Place
(在不正确的位置),数据对象是:数组
和链表
。 -
要求:
待排序的数据必须是有确定范围的整数
。
时间复杂度
-
平均
:O(n + k) -
最好
:O(n + k) -
最坏
:O(n2) -
k
:是待排序的数组(链表)中的最大的元素减去最小的元素加上一
,即:输入的元素的范围
。
空间复杂度
- O(k),需要额外的空间存储元素出现的次数。
原理
-
找出待排序的数组(链表)中的
最大的元素(max)
和最小的元素(min)
。 -
创建
数组 C[max-min+1]
,用来存储待排序的数组(链表)每个元素出现的次数
,存放方式:值减 min 等于 i 的元素出现的次数存放在 C[i]。 -
也就是说:
i = 元素值 - min
。 -
数组 C 的长度
取决于:待排序数组(链表)中数据的范围,即max - min + 1
。 -
累加所有元素出现的次数(从数组 C 的第一个元素累加到最后一个元素,默认项是没有值的)
,这个值就是目标数组(链表)的长度。 -
遍历数组 C 输出元素,其中
元素值为:i + min,元素出现次数为:C[i]
。(输出 C[i] 次 i+min)
桶排序(箱排序,Bucket Sort)
排序方式
-
Out-Place
(在不正确的位置),数据对象是:数组
和链表
。 -
桶排序
是计数排序
的升级版
。
时间复杂度
-
平均
:O(n + k) -
最好
:O(n + k) -
最坏
:O(n2)
空间复杂度
- O(n + k)
原理
其一:箱子一一对应元素
-
设置一个
定量的数组当作空桶(空箱)
,这样的空桶(空箱)有 k 个
。 -
设定每个桶的对应一个元素(范围内的)
,那么桶的数量
大于等于:排序的数组(链表)中的最大的元素(max)- 最小的元素(min)+ 1
。 -
遍历输入数据,并且把数据一个一个放到对应的桶里去,
相同的元素放到同一个桶中,不同的元素放在不同的桶中
。 -
那么很可能会有桶是空的,
对每个非空的桶进行桶间排序(每个桶要按照桶内元素来排序)
,空桶没有后续操作了。 -
遍历排序好的非空桶的序列,按照排序顺序将非空的桶里的数据拼接起来
,桶内的多个元素拼接到一起
。
其二:箱子对应一定范围内的元素
-
设置一个
定量的数组当作空桶(空箱)
,这样的空桶(空箱)有 k 个
。 -
设定每个桶的对应一定区间内的元素(范围内的)
,区间一般是同样长度的,那么桶的数量
大于等于:排序的数组(链表)中的(最大的元素(max)- 最小的元素(min)+ 1)/区间长度
。 -
遍历输入数据,并且把数据一个一个放到对应的桶里去,
相同区间的元素放到同一个桶中,不同区间的元素放在不同的桶中
。 -
那么很可能会有桶是空的,
对每个非空的桶进行桶间排序(每个桶要按照桶内元素区间来排序)
,空桶没有后续操作了。 -
桶内元素分别要进行排序
,方便进行后续遍历。 -
遍历排序好的非空桶的序列,按照排序顺序将非空的桶里的数据拼接起来
,桶内的区间元素按照排序好的顺序拼接到一起
。
基数排序(Radix Sort)
排序方式
-
Out-Place
(在不正确的位置),数据对象是:数组
和链表
。 -
字母
k
表示排序的数组元素的最大位数
(就是最大值的位数)。 -
基数排序属于
分配式排序(Distribution Sort)
,又称桶子法(Bucket Sort 或 Bin Sort)
。 -
基数排序
是桶排序
的扩展
。 -
要求:
待排序的数据必须是有确定范围的整数
。
时间复杂度
-
平均
:O(n * k) -
最好
:O(n * k) -
最坏
:O(n * k)
空间复杂度
- O(n + k)
原理
-
基数排序的
原理是:将整数按位数切割成不同的数字,然后按每个位数分别比较
。 -
确定排序的数组(链表)元素的
最大位数(Max 位)
(就是最大值的位数),表示需要执行循环的轮数
。 -
创建
十个桶,表示:0~9
,桶是用队列来实现的
。(所有的数字元素都是由 0~9 的十个数字组成) -
根据
循环执行轮数
依次判断每个元素的个位、十位、百位至 Max 位
。 -
循环时,根据元素的
当前比较位(个位、十位、百位至 Max 位)
来选择该元素要放入那个桶;该元素没有该比较位时存入 0 号数据桶
,该元素该比较位数值等于 1 时存入 1 号数据桶
,以此类推。 -
每轮循环结束后,
按照 0~9 号顺序,将数据桶中的数据存回原数组(链表)中
,而同一个桶内的元素连续存放,出桶顺序就是进桶顺序(队列)
。 -
比较完最大位数(Max 位),元素归位到原数组(链表)中,算法结束
。
疑问
-
如何保证
位数相同、数值不等、若干位相同的元素按照数值大小排列
呢?(如:11 和 12、21 和 31 等)-
11 和 12 是高位相同
,在个位排序
时,11 去到 1 数字桶,12 去到 2 数字桶,归位到原数组时,12 一定在 11 后面。 -
11 和 12 在
十位排序
时,11 和 12 都去到 1 数字桶,因为桶是队列结构,归位到原数组时,12 也一定在 11 后面。 -
11 和 12 在
百位排序及之后 m 位排序
时,11 和 12 都去到 0 数字桶,因为桶是队列结构,归位到原数组时,顺序不变
。 -
21 和 31 是低位相同
,在个位排序
时,顺序不变;在十位排序
时,21 去到 2 数字桶,31 去到 3 数字桶,归位到原数组时,31 一定在 21 后面。 -
21 和 31 在
百位排序及之后 m 位排序
时,21 和 31 都去到 0 数字桶,因为桶是队列结构,归位到原数组时,顺序不变。
-
不稳定的排序算法
选择排序(Selection Sort)
排序方式
In-Place
(在正确的位置),数据对象是:数组
和链表
。以链表为对象时,是稳定的算法。
时间复杂度
-
平均
:O(n2) -
最好
:O(n2) -
最坏
:O(n2)
空间复杂度
- O(1),
只需要添加一个元素用来交换数组(链表)元素
,与数组(链表)大小无关。
原理
-
我们将数组(链表)中的数据分为两个区间:
已排序区间
和未排序区间
。初始已排序区间没有元素。 -
选择排序
每次会从未排序区间中找到最小(最大)的元素,将其放到已排序区间的末尾
,保证已排序区间的数据一直有序。 -
重复这个过程,
直到未排序区间中元素为空
,算法结束。 -
注意:无论怎样都
无法提前结束的排序算法的
。
堆排序(Heap Sort)
- 堆:是一种常用的
树形结构
,是一种特殊的完全二叉树
,当且仅当满足所有节点的值总是不大于或不小于其父节点的值
的完全二叉树。
排序方式
In-Place
(在正确的位置),数据对象是:数组
。
时间复杂度
-
平均
:O(n log n) -
最好
:O(n log n) -
最坏
:O(n log n)
空间复杂度
- O(1),
只需要添加一个元素用来交换数组元素
,与数组大小无关。
原理
-
堆排序是指:利用
堆
这种数据结构所设计的一种排序算法。 -
每个
结点的值都大于等于其左右子结点的值
,称为大顶堆(大根堆)
。 -
每个
结点的值都小于等于其左右子结点的值
,称为小顶堆(小根堆)
。 -
将
初始待排序数组构建成大顶堆
,此堆为初始的无序区
,初始的有序区为空
。 -
将
堆顶元素R[0]
与最后一个元素R[n-1]
交换,此时无序区减一得到新的无序区(R[0]、R[1]、...、R[n-2])
;有序区加一得到新的有序区(R[n-1])
。且满足R[0,1,2…n-2] <= R[n-1]。 -
由于交换后新的堆顶R[0]可能违反堆的性质,因此
需要对当前无序区(R[0]、R[1]、...、R[n-2])进行调整,重新排序为新堆
。 -
不断重复
交换元素
和重排堆
的操作,直到无序区元素为一个或有序区元素为 n-1 个
时,完成排序。
快速排序(Quick Sort)
排序方式
In-Place
(在正确的位置),数据对象是:数组
。
时间复杂度
-
平均
:O(n log n) -
最好
:O(n log n) -
最坏
:O(n2),当基准点每次都选择到最坏的情况。(基准点每次都是数组的最边缘的元素)
空间复杂度
- O(log n)
原理
-
快速排序的
基本思想是:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,已达到整个序列有序
。 -
从数组中挑出一个元素,设定为
基准
(pivot),是一个分界值。 -
重新排序数组,
所有元素比基准值小的摆放在基准前面
,所有元素比基准值大的摆在基准的后面
,相同的数可以到任一边。 -
以
基准值
为界限,将重新排序好的数组分为前后两个子数组(基准可以放到任一个子数组中),两个子数组又可以执行找基准、重新排序
的操作。 -
重复上述过程,
当各部分排序完成后,整个数组就排序完成了
。
优化快速排序
-
最理想的基准点是:
被基准点分开的两个子数组中,数据的数量差不多
。 -
优化快速排序就是指:尽量每次寻找到的基准点都接近最理想的基准点。
三数取中法
-
我们从数组的首、尾、中间,分别取出一个数,然后对比大小,
取这 3 个数的中间值作为分区点(基准点)
。 -
这样每间隔某个固定的长度,取数据出来比较,
将中间值作为分区点的分区算法,肯定要比单纯取某一个数据更好
。 -
但是,如果要排序的数组比较大,那三数取中可能就不够了,可能要五数取中或者十数取中。
随机法
-
随机法就是每次从要排序的区间中,
随机选择一个元素作为分区点(基准点)
。 -
这种方法
并不能保证每次分区点都选的比较好
,但是从概率的角度来看,也不大可能会出现每次选择的分区点(基准点)都很差的情况
。 -
所以
平均情况下,这样选的分区点是比较好的
。 -
时间复杂度退化为最糟糕的 O(n²) 的情况,出现的可能性不大。
希尔排序(Shell’s Sort)
排序方式
-
In-Place
(在正确的位置),数据对象是:数组
。 -
是
插入排序
的升级版。 -
希尔排序
适合初始数据基本无序
的数据。 -
插入排序
适合初始数据偏向有序
的数据。
时间复杂度
-
平均
:O(n log n) -
最好
:O(n log2 n) -
最坏
:O(n log2 n)
空间复杂度
- O(1)
原理
-
把
待排序的数组
按照下标的一定增量(增量:gap)进行分组
。(就是同组的元素的下标差是增量:gap
) -
对
每组元素进行直接插入排序
,元素间只能在组内互相交换位置
,不能改变每个组所包含的数组下标集。 -
增量 gap 每次循环会变为原来的一半(取整),即:
gap = gap / 2
。 -
随着增量 gap 的减少,分组越少,组内元素越多,当
增量 gap 减至 1 时
,整个序列恰好被分为一组,对组内元素排序后,算法终止。 -
推荐 gap 初始值取数组长度的一半(取整)
。