在博文各个排序算法的实现与优化(含动画演示)已经将各种排序算法的实现进行了讲解,本文将重点针对其适用场景进行介绍,在介绍各排序算法的使用场景之前,先来温习一下跟时间复杂度有关的一些名词概念:
逆序对:设 A 为一个有 n 个数字的有序集 (n>1),其中所有数字各不相同。如果存在正整数 i, j 使得 1 ≤ i < j ≤ n 而且 A[i] > A[j],则 <A[i], A[j]> 这个有序对称为 A 的一个逆序对,也称作逆序数。在未知的状态下,每两个元素为逆序对的概率为50%,所以一个长度为n的未知数组的逆序对为((n-1)(n-2)/2*50\%)
交换操作:交换操作分为三次赋值操作
排序算法的使用场景
下面博主便从是否为原地排序
,是否为稳定排序
,平均时间复杂度
,何时时间复杂度最低(何时最优)
,何时时间复杂度最高(何时不适合)
五个方面进行分析,这几种排序算法到底适用与否。
冒泡排序
- 是否为原地排序:冒泡操作是在原数组之上进行的,只需要常量级的临时空间用于交换,空间复杂度为(O(1)),是原地排序算法。
- 是否为稳定排序:当有相邻的两个元素大小相等的时候,不做交换,即可以保证其为稳定的排序算法
- 平均时间复杂度:冒泡排序交换数组的逆序对个数一样,而比较操作为k*n次,总得时间复杂度为:(O(n) = 3*(n-1)(n-2)/2*50\% + k*n = n^2)
- 何时时间复杂度最低:所需排序的数组是有序的,只需一次冒泡操作,其时间复杂度为(O(n))。
- 何时时间复杂度最高:所需排序的数组是倒序排列的,需要n次冒泡操作,其时间复杂度为(O(n^2))。
插入排序
- 是否为原地排序:插入操作是在原数组之上进行的,只需要常量级的临时空间用于交换,空间复杂度为(O(1)),是原地排序算法。
- 是否为稳定排序:将大小相等的两个元素按原来的顺序合理执行插入操作,即可以保证其为稳定的排序算法
- 平均时间复杂度:由于在数组中插入元素的平均时间复杂度为:(O((n-1)(n-2)/2/n)=O(n)),所以n个元素进行插入操作的时间复杂度为:(n*O(n)=O(n^2))
- 何时时间复杂度最低:所需排序的数组是有序的,只需执行一次从头到尾的插入操作,而每次插入操作都这是进行一次比较,实际上是(n)次比较操作,其时间复杂度为(O(n))。
- 何时时间复杂度最高:所需排序的数组是倒序排列的,第n个元素需要n-1次比较操作和n次移位操作,操作次数共有:((n-1)(n-2)/2 + n(n-1)/2 = n^2 -2n+1)次,所以其时间复杂度为(O(n^2))。
选择排序
- 是否为原地排序:选择排序的交换操作是在原数组之上进行的,只需要常量级的临时空间用于交换,空间复杂度为(O(1)),是原地排序算法。
- 是否为稳定排序:因为每次都是选择未排序部分的最小值与未排序部分的第一个值进行交换,这样的交换操作无法使用大小维持原来的顺序,所以不是稳定的排序算法
- 平均时间复杂度:无论数组组成如何,都需要将未排序区域全部遍历比较,然后进行交换,所以当未排序区域长度为n时,需要n-1次比较操作和(1 或 0)次交换操作,所以时间复杂度为(O((n-1)(n-2)/2+3*k)=O(n^2))
- 何时时间复杂度最低:与平均时间复杂度一样
- 何时时间复杂度最高:与平均时间复杂度一样
归并排序
- 是否为原地排序:由于归并操作需要创建n个内存空间用于存储归并的数组,所以不是原地排序算法,同时其为递归操作产生递归栈使用的空间复杂度为O(logn)
- 是否为稳定排序:在归并操作中保持相同元素的原排列顺序,即可以保证其为稳定的排序算法
- 平均时间复杂度:平均复杂度需要根据推导得出,由于归并排序采取分而治之的思想,所以时间复杂度也可以进行分解为两倍的子排序时间复杂度T(n/2)加本归并过程的时间复杂度n即(O(n) = 2*O(n/2)+n = 2^k * O(n/2^k) + 3 * k * n),当子问题分解为一个元素时,(n/2^k = 1),即(k=log n),进一步可以得出(O(n) = n + 3 * nlog n = n log n)
- 何时时间复杂度最低:通过上述平均复杂度的分析可以得出,执行效率与要排序的原始数组的有序程度无关,所以其时间复杂度是非常稳定的,不管是最好情况、最坏情况,还是平均情况,时间复杂度都是O(nlogn).
- 何时时间复杂度最高:与平均时间复杂度一样
快速排序
- 是否为原地排序:每一段的快速排序都是通过交换操作是在原数组之上进行的,只需要常量级的临时空间用于交换,空间复杂度为(O(1)),是原地排序算法,但是由于快排本身为递归调用,所以产生的递归栈最好和平均情况下的空间复杂度为O(logn),最坏情况下的空间复杂度为O(n)。
- 是否为稳定排序:因为每次都要根据所选的分段值将不同的两两元素进行交换,这样的交换操作无法使用大小维持原来的顺序,所以不是稳定的排序算法
- 平均时间复杂度:由于快速排序的分段与分段值有关,当数组未知时,所选择的元素与其他元素的大小关系也未知,应当是对半分段,如归并排序一样,同时与归并排序不同,多了分段操作(需要n次比较操作),所以其平均时间复杂度跟归并排序一样为(O(n) = n + m * n log n = n log n)
- 何时时间复杂度最低:根据树的分布特点如果不是完全二叉树,其深度一定更高,所以对半分是最佳的状态,最终的子问题必为1个元素,所以分解子问题本身操作时间复杂度不变为(O(n)),而对于分段操作中的比较操作仍然是n次,但是交换操作在有序的状态可以省略,所以对于有序数组且每次取中位值,时间消耗最小,但时间复杂度仍然是(O(n) = n + nlog n = n log n),因为m作为常值趋近于1与否不影响。
- 何时时间复杂度最高:根据树的分布特点,当其退化为链表时深度最高,相应的快排需要执行n次的分段操作即(k=n),但是不需要交换操作(不太严谨,可能非常值次数的交换操作更合适),所以时间复杂度为(O(n) = n + n * n = n^2)
- 读到这里,相信大部分读者会有疑问为什么常用快速排序而不用归并排序,这是因为可以采取很多操作避免快排中的极端情况,同时归并排序需要O(n)的空间复杂度,快排只需要O(1)的空间复杂度用于临时交换。值得一提的是,使归并排序用于链表排序时可以避免O(n)时间复杂度这个缺点。
表格总结
排序算法 | 最坏时间复杂度 | 最好时间复杂度 | 平均时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|---|---|---|
插入排序 | O(n2) | O(n) | O(n2) | O(1) | ✔ |
冒泡排序 | O(n2) | O(n) | O(n2) | O(1) | ✔ |
选择排序 | O(n2) | O(n2) | O(n2) | O(1) | ✘ |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | ✔ |
快速排序 | O(n2) | O(nlogn) | O(nlogn) | O(logn) | ✘ |