本文记录了几种经典的排序算法,并对排序算法的性能进行了总结,如果有哪里写的不妥的,还请大家指正批评。同时,也欢迎大家提供更简单有效的算法。
排序算法的性能评估
- 稳定性(假设记录
a
和记录b
都位于待排序的序列中)
(1)稳定:如果a
本来就在b
前面,且a = b
,排序之后a
仍然在b
前面;
(2)不稳定:如果a
本来就在b
前面,且a = b
,排序之后a
可能出现在b
后面; - 待排序的记录是否全部被放置在内存中
(1)内排序:在整个排序过程中,待排序的所有记录全部被放置在内存中;(文中的所有排序算法都属于内排序)
(2)外排序:由于排序的记录个数太多,不能同时放置在内存,整个排序过程需要在内外存之间多次交换数据才能进行; - 时间复杂度:排序算法的时间开销
- 空间复杂度:排序算法所需的存储空间
- 算法本身的复杂度
排序算法(7 种)
下面,将根据 程杰的《大话数据结构》 ,从易到难对7种排序算法进行整理归纳
1、冒泡排序(Bubble Sort)
基本思想: 两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。
(1)初级版
|
|
上面代码中,每一次外循环,都是将第 i
个位置的记录, 与它后面的记录进行比较。如果大,则进行交换,这样一次大循环之后,第 i
个位置的记录与后面位置的记录相比就是最小的。
(2)正宗版
每执行一次外循环,会从排序数组末尾开始两两比较,把较小的记录交换到前面,知道找到最小值放在第 i
个位置。这样在寻找最小值的过程中,也会把较小值移到排序数组的前面,显然这一点要由于冒泡排序初级版。
(3)优化版
|
|
假设待排序的序列为 {2,1,3,4,5,6,7,8,9}
, 除了第一和第二个记录需要交换外,其他都已经是正确的顺序了。也就是说,后面的记录其实不需要进行交换,也不需要进行比较。这里设置 flag
变量作为标记,每次外循环的时候判断前一次循环中的内循环是否进行数据交换。如果没有进行数据交换,则退出外循环,说明后面已经是有序的了,不需要再进行比较。
经过这样的改进,冒泡排序在性能上就有了一定的提升,可以避免因已经有序的情况下的无意义循环判断。
这只是冒泡排序算法的一种改进,网上还有很多其他的改进算法,可以参考十大经典排序算法的JS 版。
冒泡排序性能评估
- 稳定性:不稳定;
- 待排序的记录是否全部被放置在内存中:内排序;
- 时间复杂度
(1)最好的情况:O(n)——已经是正序的;
(2)最坏的情况:O(n^2)——正好是逆序的;
(3)平均的情况:O(n); - 空间复杂度:O(1);
- 算法本身的复杂度:简单;
2、选择排序(Selection Sort)
基本思想: 每一趟通过 n-i
次关键字之间的比较,从 n-i+1
个记录中选出关键字最小的记录,并和第 i
个记录进行交换;
|
|
选择排序性能评估
- 时间复杂度:无论最好最差,都是 O(n^2);
- 空间复杂度:O(1);
- 算法本身复杂度:简单;
- 性能略优于冒泡排序,因为交换移动次数比较少;
3、插入排序(Insertion Sort)
基本思想: 将未排序的无序表中的一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增 1 的有序表;
|
|
插入排序性能评估
- 时间复杂度
(1)最好的情况:O(n)——已经是正序的,比较 n-1 次,没有移动;
(2)最坏的情况:O(n^2)——正好是逆序的;
(3)平均的情况:O(n^2); - 空间复杂度:O(1)
- 算法本身的复杂度:简单
- 插入排序比冒泡和选择排序的性能要好一些
4、希尔排序(Shell Sort)
—— 基于插入排序进行改进的算法
基本思想: 采用跳跃分割策略,将相距某个“增量”的记录组成一个子序列,然后在子序列内分别进行插入排序;
关键: “增量”的选取;
|
|
希尔排序性能评估
- 稳定性:不稳定(跳跃式比较并移动);
- 时间复杂度:O(n^(3/2)),优于插入排序的O(n^2);
- 空间复杂度:O(1);
- 算法本身的复杂度:较前面三种排序算法稍复杂;
5、堆排序(Heap Sort)
—— 基于选择排序进行改进的算法
堆是具有下列性质的完全二叉树: 每个节点的值都大于或等于其左右孩子的节点的值,称为 大顶堆;或者每个节点的值都小于或蒸鱼其左右孩子的值,称为 小顶堆;
基本思想:(假设利用大顶堆) 将待排序的序列构成一个大顶堆。此时,整个序列的最大值就是对顶的根结点,将它移步(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的 n-1
个序列重新构造成一个堆,这样就会得到 n
个元素中的次大值。如此反复执行,就能得到一个有序序列了。
步骤:
- 将无序数列构建成一个大顶堆;(初始构建堆)
- 逐步将每个最大值的根结点与末尾元素进行交换,并再次调整其成为大顶堆;(多次调整堆)
|
|
堆排序性能评估
- 稳定性:不稳定;
- 时间复杂度:无论最好、最坏情况,时间复杂度都是:O(nlogn);
- 空间复杂度:O(1);
- 算法本身的复杂度:复杂;
6、归并排序(Merging Sort)
“归并”: 在数据结构中,指的是将两个或两个以上的有序表组合成一个新的有序表;
基本思想: 假设初始序列含有 n
个记录,则可以看成是 n
个有序的子序列,每个子序列的长度为 1
,然后两两归并,得到 x
(一个不小于 n/2
的整数)个长度为 2
或 1
的有序子序列,再两两归并,……,如此重复,直至的代一个长度为 n
的有序序列为止;
(1)递归实现
|
|
归并排序性能分析
- 稳定性:两两比较,不存在跳跃,稳定;
- 时间复杂度:无论最好、最坏情况,时间复杂度都是:O(nlogn);
- 空间复杂度:O(n+logn);
- 算法本身的复杂度:复杂;
- 归并排序是一种比较占内存,但却效率高且稳定的算法;
(2)非递归实现 (不太明白,后期再加上)
7、快速排序(Quick Sort)
—— 基于冒泡排序进行改进的算法(增大了记录的比较移动的距离)
基本思想: 通过一趟排序,将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则分别对这两部分记录继续进行排序,以达到整个序列有序的目的;
|
|
快速排序性能评估
- 稳定性:不稳定(关键字的比较和交换都是跳跃进行的)
- 时间复杂度
(1)最好的情况:O(nlogn);
(2)最坏的情况:O(n^2);
(3)平均的情况:O(nlogn); - 空间复杂度:O(nlogn);
- 算法本身的复杂度:复杂;
以上代码只是最基本的快速排序法,书中还记录了 4
种优化方案,分别为:优化选取枢轴、优化不必要的交换、优化小数组时的排序方案、优化递归操作,这里不一一详细展开,如果有需要,可以查询《大话数据结构》P422 “快速排序优化”
一节。
排序算法总结
分类
以上 7
种排序算法均属于内排序,本文不涉及外排序;
按照借助的主要操作,可以进行如下分类:
性能对比
排序方式 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n^2) | O(n) | O(n^2) | O(1) | 稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 稳定 |
插入排序 | O(n^2) | O(n) | O(n^2) | O(1) | 稳定 |
希尔排序 | O(nlogn)~O(n^2) | O(n^(3/2)) | O(1) | O(1) | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
快速排序 | O(nlogn) | O(nlogn) | O(n^2) | O(logn)~O(n) | 不稳定 |
- 从平均情况来看,最后
3
种改进算法要胜过希尔排序,并远远超过前3
种简单算法; - 从最好情况来看,冒泡和插入排序算法要比其他算法更胜一筹,也就是说如果待排序的序列基本有序,则可以选择这两种排序算法;
- 从最坏情况来看,堆排序和归并排序要由于快速排序以及其他简单排序;
- 从空间复杂度来看,除了归并排序和快速排序之外,其余算法对辅助空间的要求一致,都是
O(1)
;如果排序过程中,对空间复杂度有要求,则建议选前面的5
种排序算法; - 从稳定性来看,改进算法中,只有归并排序是稳定的,而简单排序都是稳定的;
- 从待排序的记录的个数看,如果待排序的个数
n
越小,则采用简单排序算法比较合适;如果待排序的个数n
越大,则采用改进排序算法更合适。