目录
内部排序
前言
3.内部排序各种算法性能分析
3.1直接插入排序
3.2折半插入排序
3.3希尔排序
3.4冒泡排序
3.5快速排序
3.6简单选择排序
3.7堆排序
3.8归并排序
3.9基数排序
3.10计数排序
内部排序
前言
这篇文章总结了所有内部排序的适用场景,性质特点,以及空间和时间复杂度的考量;
前半部分是对内部排序的总结,后半部分是对所有内部排序的具体性能分析;
1.内部算法>排序算法的比较
1.1各种算法>排序算法的特点、比较和适用场景
从时间复杂度看:
简单选择排序、直接插入排序和冒泡排序平均情况下的时间复杂度都为O(n²),且实现过程也较为简单,但直接插入排序和冒泡排序最好情况下的时间复杂度可以达到O(n),而简单选择排序则与序列的初始状态无关。
希尔排序作为插入排序的拓展,对较大规模的数据都可以达到很高的效率,但目前未得出其精确的渐近时间。
堆排序利用了一种称为堆的数据结构,可以在线性时间内完成建堆,且在 O(nlog₂n)内完成排序过程。
快速排序基于分治的思想,虽然最坏情况下的时间复杂度会达到 O(n²),但快速排序的平均性能可以达到 O(nlog₂n),在实际应用中常常优于其他算法>排序算法。
归并排序同样基于分治的思想,但由于其分割子序列与初始序列的排列无关,因此它的最好、最坏和平均时间复杂度均为 O(nlog₂n)。
从空间复杂度看:
简单选择排序、插入排序、冒泡排序、希尔排序和堆排序都仅需借助常数个辅助空间。
快速排序需要借助一个递归工作栈,平均大小为 O(log₂n),当然在最坏情况下可能会增长到 O(n)。
二路归并排序在合并操作中需要借助较多的辅助空间用于元素复制,大小为 O(n),虽然有方法能克服这个缺点,但其代价是算法会很复杂而且时间复杂度会增加。
1.2算法>排序算法的稳定性判断及改进
从稳定性看:
插入排序、冒泡排序、归并排序和基数排序是稳定的算法>排序算法,
而简单选择排序、快速排序、希尔排序和堆排序都是不稳定的算法>排序算法。
平均时间复杂度为 O(nlog₂n)的稳定算法>排序算法只有归并排序,对于不稳定的算法>排序算法,只需举出一个不稳定的实例即可。
1.3更适合采用顺序存储的算法>排序算法
从适用性看:
折半插入排序、希尔排序、快速排序和堆排序适用于顺序存储。
直接插入排序、冒泡排序、简单选择排序、归并排序和基数排序既适用于顺序存储,又适用于链式存储。
1.4根据排序的中间过程判断所采用的算法>排序算法
从过程特征看:
采用不同的算法>排序算法,在一趟或几趟处理后的排序结果通常是不同的,考研题中经常出现给出一个待排序的初始序列和已部分排序的序列,问其采用何种算法>排序算法。
这就要对各类算法>排序算法的过程特征十分熟悉,如冒泡排序、简单选择排序和堆排序在每趟处理后都能产生当前的最大值或最小值,
而快速排序一趟处理至少能确定一个元素的最终位置等。
下表列出了各种算法>排序算法的时空复杂度和稳定性情况,其中空间复杂度仅列举了平均情况的复杂度,因为希尔排序的时间复杂度依赖于增量函数,所以无法准确给出其时间复杂度。
1.5各种算法>排序算法的性质
算法种类 | 时间复杂度 | 空间复杂度 | 是否稳定 | ||
最好情况 | 平均情况 | 最坏情况 | |||
直接插入排序 | O(n) | O(n²) | O(n²) | O(1) | 是 |
冒泡排序 | O(n) | O(n²) | O(n²) | O(1) | 是 |
简单选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 否 |
希尔排序 | O(1) | 否 | |||
快速排序 | O(nlog₂n) | O(nlog₂n) | O(n²) | O(log₂n) | 否 |
堆排序 | O(nlog₂n) | O(nlog₂n) | O(nlog₂n) | O(1) | 否 |
二路归并排序 | O(nlog₂n) | O(nlog₂n) | O(nlog₂n) | O(n) | 是 |
基数排序 | O(d(n+r)) | O(d(n+r)) | O(d(n+r)) | O(r) | 是 |
2.内部算法>排序算法的应用
2.1选取算法>排序算法时需要考虑的因素
- 待排序的元素个数 n。
- 待排序的元素的初始状态。
- 关键字的结构及其分布情况。
- 稳定性的要求。
- 存储结构及辅助空间的大小限制等。
2.2算法>排序算法小结
1)
- 若 n较小,可采用直接插入排序或简单选择排序。
- 由于直接插入排序所需的记录移动次数较简单选择排序的多,因此当记录本身信息量较大时,用简单选择排序较好。
2)
- 若n较大,应采用时间复杂度为 O(nlog₂n)的算法>排序算法:快速排序、堆排序或归并排序。
- 当待排序的关键字随机分布时,快速排序被认为是目前基于比较的内部算法>排序算法中最好的算法。
- 堆排序所需的辅助空间少于快速排序,且不会出现快速排序可能的最坏情况,这两种排序都是不稳定的。
- 若要求稳定且时间复杂度为 O(nlog₂n),可选用归并排序。
3)
- 若文件的初始状态已按关键字基本有序,则选用直接插入或冒泡排序为宜。
4)
- 在基于比较的算法>排序算法中,每次比较两个关键字的大小之后,仅出现两种可能的转移,因此可以用一棵二叉树来描述比较判定过程,
- 由此可以证明:当文件的n个关键字随机分布时,任何借助于“比较”的算法>排序算法,至少需要 O(nlog₂n)的时间。
5)
- 若n很大,记录的关键字位数较少且可以分解时,采用基数排序较好。
6)
- 当记录本身信息量较大时,为避免耗费大量时间移动记录,可用链表作为存储结构。
3.内部排序各种算法性能分析
3.1直接插入排序
空间效率:仅使用了常数个辅助单元,因而空间复杂度为O(1)。
时间效率:在排序过程中,向有序子表中逐个地插入元素的操作进行了n-1趟,
- 每趟操作都分为比较关键字和移动元素,而比较次数和移动次数取决于待排序表的初始状态。
- 在最好情况下,表中元素已经有序,此时每插入一个元素,都只需比较一次而不用移动元素,因而时间复杂度为 O(n)。
- 在最坏情况下,表中元素顺序刚好与排序结果中的元素顺序相反(逆序),总的比较次数达到最大,总的移动次数也达到最大,总的时间复杂度为O(n²)。
- 平均情况下,考虑待排序表中元素是随机的,此时可以取上述最好与最坏情况的平均值作为平均情况下的时间复杂度,总的比较次数与总的移动次数均约为n²/4。
- 因此,直接插入算法>排序算法的时间复杂度为 O(n²)。
稳定性:因为每次插入元素时总是从后往前先比较再移动,所以不会出现相同元素相对位置发生变化的情况,即直接插入排序是一个稳定的算法>排序算法。
适用性:直接插入排序适用于顺序存储和链式存储的线性表,采用链式存储时无须移动元素。
3.2折半插入排序
空间效率:在排序过程中只需要额外使用常数级别的额外空间,不随待排序数组的大小而变化,所以空间复杂度为O(1)。
时间效率:折半插入排序仅减少了比较元素的次数,该比较次数与待排序表的初始状态无关,仅取决于表中的元素个数n;
而元素的移动次数并未改变它依赖于待排序表的初始状态。
因此,折半插入排序的时间复杂度仍为 O(n²),但对于数据量不很大的排序表,折半插入排序往往能表现出很好的性能。
适用性:折半插入排序仅适用于顺序存储的线性表。
3.3希尔排序
空间效率:仅使用了常数个辅助单元,因而空间复杂度为O(1)。
时间效率:因为希尔排序的时间复杂度依赖于增量序列的函数,这涉及数学上尚未解决的难题,所以其时间复杂度分析比较困难。
- 当n在某个特定范围时,希尔排序的时间复杂度约为。
- 在最坏情况下希尔排序的时间复杂度为O(n²)。
稳定性:当相同关键字的记录被划分到不同的子表时,可能会改变它们之间的相对次序,
因此希尔排序是一种不稳定的算法>排序算法。例如,图82中49与49的相对次序已发生了变化。
适用性:希尔排序仅适用于顺序存储的线性表。
3.4冒泡排序
冒泡排序的性能分析如下:
空间效率:仅使用了常数个辅助单元,因而空间复杂度为O(1)。
时间效率:当初始序列有序时,显然第一趟冒泡后 flag 依然为 false(本趟没有元素交换),从而直接跳出循环,比较次数为n-1,移动次数为0,
- 从而最好情况下的时间复杂度为 O(n);
- 当初始序列为逆序时,需要进行n-1趟排序,第 i 趟排序要进行n-i次关键字的比较,而且每次比较后都必须移动元素3次来交换元素位置。
- 这种情况下,
- 比较次数=
- 移动次数=
- 从而,最坏情况下的时间复杂度为 O(n²),平均时间复杂度为 O(n²)。
稳定性:由于 i>j 且 A[i]=A[j] 时,不会发生交换,因此冒泡排序是一种稳定的算法>排序算法。
适用性:冒泡排序适用于顺序存储和链式存储的线性表。
3.5快速排序
- 空间效率:由于快速排序是递归的,因此需要借助一个递归工作栈来保存每层递归调用的必要信息,其容量与递归调用的最大层数一致。
- 最好情况下为 O(log₂n);
- 最坏情况下,要进行n-1次递归调用,因此栈的深度为 O(n);
- 平均情况下,栈的深度为 O(log₂n)。
- 时间效率:快速排序的运行时间与划分是否对称有关,快速排序的最坏情况发生在两个区域分别包含n-1个元素和0个元素时,这种最大限度的不对称性若发生在每层递归上,即对应于初始排序表基本有序或基本逆序时,就得到最坏情况下的时间复杂度为 O(n²)。
- 有很多方法可以提高算法的效率:一种方法是尽量选取一个可以将数据中分(对称平分)的枢轴元素,如从序列的头尾及中间选取三个元素,再取这三个元素的中间值作为最终的枢轴元素;或者随机地从当前表中选取枢轴元素,这样做可使得最坏情况在实际排序中几乎不会发生。
- 在最理想的状态下,即 Partition()能做到最平衡的划分,得到的两个子问题的大小都不可能大于 n/2,在这种情况下,快速排序的运行速度将大大提升,此时,时间复杂度为 O(nlog₂n)。
- 好在快速排序平均情况下的运行时间与其最佳情况下的运行时间很接近,而不是接近其最坏情况下的运行时间。
- 快速排序是所有内部算法>排序算法中平均性能最优的算法>排序算法。
- 稳定性:在划分算法中,若右端区间有两个关键字相同,且均小于基准值的记录,则在交换到左端区间后,它们的相对位置会发生变化,即快速排序是一种不稳定的算法>排序算法。
- 例如,表L=(3,2,2),经过一趟排序后L={2,2,3},最终排序序列也是L={2,2,3},显然,2与2的相对次序已发生了变化。
- 适用性:快速排序仅适用于顺序存储的线性表。
3.6简单选择排序
空间效率:仅使用常数个辅助单元,所以空间效率为O(1)。
时间效率:从上述伪码中不难看出,
- 在简单选择排序过程中,元素移动的操作次数很少,不会超过 3(n-1)次,最好的情况是移动0次,此时对应的表已经有序;
- 但元素间比较的次数与序列的初始状态无关,始终是 n(n-1)/2 次,因此时间复杂度始终是 O(n²)。
稳定性:
- 在第 i 趟找到最小元素后,和第 i 个元素交换,可能会导致第 i 个元素与含有相同关键字的元素的相对位置发生改变。
- 例如,表 L={2,2,1},经过一趟排序后 L={1,2,2),最终排序序列也是L={1,2,2},显然,2与2的相对次序已发生变化。
- 因此,简单选择排序是一种不稳定的算法>排序算法。
适用性:简单选择排序适用于顺序存储和链式存储的线性表,以及关键字较少的情况。
3.7堆排序
空间效率:仅使用了常数个辅助单元,所以空间复杂度为O(1)。
时间效率:建堆时间为 O(n),之后有n-1次向下调整操作,每次调整的时间复杂度为 O(h),
所以在最好、最坏和平均情况下,堆排序的时间复杂度为O(nlog₂n)。
稳定性:进行筛选时,有可能把后面相同关键字的元素调整到前面,所以堆算法>排序算法是一种不稳定的算法>排序算法。
- 例如,表L={1,2,2),构造初始堆时可能将2交换到堆顶,此时L={2,1,2},最终排序序列为 L={1,2,2},显然,2与2的相对次序已发生变化。
适用性:堆排序仅适用于顺序存储的线性表。
3.8归并排序
空间效率:Merge()操作中,辅助空间刚好为n个单元,因此算法的空间复杂度为O(n)。
时间效率:每趟归并的时间复杂度为 O(n),共需进行趟归并,因此算法的时间复杂度为。
稳定性:由于 Merge()操作不会改变相同关键字记录的相对次序,因此二路归并算法>排序算法是一种稳定的算法>排序算法。
适用性:归并排序适用于顺序存储和链式存储的线性表。
3.9基数排序
空间效率:一趟排序需要的辅助存储空间为r (r个队列:r个队头指针和r个队尾指针),但以后的排序中会重复使用这些队列,所以基数排序的空间复杂度为O(r)。
时间效率:
- 基数排序需要进行 d趟“分配”和”收集”操作。
- 一趟分配需要遍历所有关键字,时间复杂度为 O(n);
- 一趟收集需要合并r个队列,时间复杂度为 O(r)。
- 因此基数排序的时间复杂度为 O(d(n+r)),它与序列的初始状态无关。
稳定性:每一趟分配和收集都是从前往后进行的,不会交换相同关键字的相对位置,因此基数排序是一种稳定的算法>排序算法。
适用性:基数排序适用于顺序存储和链式存储的线性表。
3.10计数排序
空间效率:计数排序是一种用空间换时间的做法。
- 输出数组的长度为n;
- 辅助的计数数组的长度为 k,空间复杂度为 O(n+k)。
- 若不把输出数组视为辅助空间,则空间复杂度为 O(k)。
时间效率:
- 上述代码的第1个和第3个for 循环所花的时间为 O(k),
- 第2个和第4个 for 循环所花的时间为 O(n),
- 总时间复杂度为 O(n+ k)。
- 因此,当 k=O(n) 时,计数排序的时间复杂度为O(n);
- 但当 k>O(nlog₂n)时,其效率反而不如一些基于比较的排序(如快速排序、堆排序等)。
稳定性:上述代码的第4个 for 循环从后往前遍历输入数组,相同元素在输出数组中的相对位置不会改变,因此计数排序是一种稳定的算法>排序算法。
适用性:计数排序更适用于顺序存储的线性表。计数排序适用于序列中的元素是整数且元素范围(0~k-1)不能太大,否则会造成辅助空间的浪费。