分类:
- 插入排序(直接插入排序、希尔排序)
- 交换排序(冒泡排序、快速排序)
- 选择排序(直接选择排序、堆排序)
- 归并排序
- 分配排序(基数排序)
所需辅助空间最多:归并排序
所需辅助空间最少:堆排序
平均速度最快:快速排序
不稳定:快速排序,希尔排序,堆排序。
直接插入排序
基本思想
将数组中的所有元素依次跟前面已经排好的元素相比较,如果选择的元素比已排序的元素小,则交换,直到全部元素都比较过为止。
代码实现
1 | /** |
复杂度
平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
---|---|---|---|
O(n²) | O(n²) | O(n²) | O(1) |
由于直接插入排序每次只移动一个元素的位, 并不会改变值相同的元素之间的排序, 因此它是一种稳定排序。
希尔排序
基本思想
该方法的基本思想是:先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的。
代码实现
1 | /** |
复杂度
平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
---|---|---|---|
$O(nlog2 n)$ | $O(nlog2 n)$ | $O(nlog2 n)$ | $O(1)$ |
由于交换位置了,希尔排序是一种不稳定排序。
简单选择排序
基本思想
- 在要排序的一组数中,选出最小的一个数与第一个位置的数交换;
- 然后在剩下的数当中再找最小的与第二个位置的数交换
- 回到步骤1,如此循环直到倒数第二个数和最后一个数比较为止。
代码实现
1 | /** |
复杂度
平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
---|---|---|---|
O(n²) | O(n²) | O(n²) | O(1) |
无论是哪种情况,哪怕原数组已排序完成,它也将花费将近n²/2次遍历来确认一遍。并且它是一种不稳定排序。
堆排序
基本思想
堆排序是一种树形选择排序,是对直接选择排序的有效改进。
此处以大顶堆为例,堆排序的过程就是将待排序的序列构造成一个堆,选出堆中最大的移走,再把剩余的元素调整成堆,找出最大的再移走,重复直至有序。
- 建堆;
- 堆顶与堆的最后一个元素交换位置,并且把最大值从堆删除;
- 重复到最后一个节点,删除最大一个,结束
代码实现
1 | /** |
复杂度
平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
---|---|---|---|
$O(n \log_{2}n)$ | $O(n \log_{2}n)$ | $O(n \log_{2}n)$ | $O(1)$ |
顺序打乱,它是不稳定排序。
冒泡排序
基本思想
重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
代码实现
1 | /** |
复杂度
平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
---|---|---|---|
O(n²) | O(n) | O(n²) | O(1) |
不会改变相同元素直接的位置,所以它是稳定的排序算法。
快速排序
基本思想
快速排序的基本思想:挖坑填数+分治法。
快速排序采用“分而治之、各个击破”的观念,
代码实现
1 | /** |
复杂度
平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
---|---|---|---|
$O(n \log_{2}n)$ | $O(n \log_{2}n)$ | $O(n^2)$ | $O(1)$ |
快速排序是通常被认为在同数量级($O(n \log_{2}n)$)的排序方法中平均性能最好的。但若初始序列按关键码有序或基本有序时,快排序反而蜕化为冒泡排序。为改进之,通常以“三者取中法”来选取基准记录,即将排序区间的两个端点与中点三个记录关键码居中的调整为支点记录。快速排序是一个不稳定的排序方法。
快速排序排序效率非常高。 虽然它运行最糟糕时将达到O(n²)的时间复杂度, 但通常平均来看, 它的时间复杂为O(nlogn), 比同样为O(nlogn)时间复杂度的归并排序还要快. 快速排序似乎更偏爱乱序的数列, 越是乱序的数列, 它相比其他排序而言, 相对效率更高.
归并排序
基本思想
归并排序算法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
代码实现
1 | /** |
复杂度
平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
---|---|---|---|
O(nlog₂n) | O(nlog₂n) | O(nlog₂n) | O(n) |
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(n log n)的时间复杂度。代价是需要额外的内存空间。
度为n的数组, 最终会调用mergeSort函数2n-1次。通过自上而下的递归实现的归并排序, 将存在堆栈溢出的风险。
基数排序
基本思想
它是这样实现的:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
基数排序按照优先从高位或低位来排序有两种实现方案:
MSD
[1] : 从最左侧高位开始进行排序。先按k1排序分组, 同一组中记录, 关键码k1相等, 再对各组按k2排序分成子组, 之后, 对后面的关键码继续这样的排序分组, 直到按最次位关键码kd对各子组排序后. 再将各组连接起来, 便得到一个有序序列。MSD方式适用于位数多的序列。
LSD
[2] :从最右侧低位开始进行排序。先从kd开始排序,再对kd-1进行排序,依次重复,直到对k1排序后便得到一个有序序列。LSD方式适用于位数少的序列。
代码实现
1 | /** |
复杂度
平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
---|---|---|---|
O(d*(n+r)) | O(d*(n+r)) | O(d*(n+r)) | O(n+r) |
其中,d 为位数,r 为基数,n 为原数组个数。在基数排序中,因为没有比较操作,所以在复杂上,最好的情况与最坏的情况在时间上是一致的,均为 O(d*(n + r))
。
基数排序更适合用于对时间, 字符串等这些整体权值未知的数据进行排序。
Tips: 基数排序不改变相同元素之间的相对顺序,因此它是稳定的排序算法。
总结
- 当原表有序或基本有序时,直接插入排序和冒泡排序将大大减少比较次数和移动记录的次数,时间复杂度可降至O(n);
- 而快速排序则相反,当原表基本有序时,将蜕化为冒泡排序,时间复杂度提高为O(n2);
- 原表是否有序,对简单选择排序、堆排序、归并排序和基数排序的时间复杂度影响不大。