持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 2 天,点击查看活动详情
常用的排序方法
-
排序算法的
稳定性是指:在需要进行排序操作的数据中,如果存在值相等的元素,在排序前后,相等元素之间的排列顺序不发生改变。 -
字母
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 初始值取数组长度的一半(取整)。