排序算法
基本概念
- 排序算法的稳定性:如果待排序的表中,存在多个关键字相同的元素,经过排序后这些具有相同关键字的元素之间的相对次序保持不变,则称这种排序算法是稳定的,反之则为不稳定。
- 内排序:排序过程中,整个表都是放在内存中处理,排序时不涉及数据的内、外交换
- 外排序:指能够处理极大量数据的排序算法,外排序处理的数据不能一次装入内存,只能放在读写较慢的外存储器(通常是硬盘)上,外排序通常采用的是一种“排序-归并”的策略。在排序阶段,先读入能放在内存中的数据量,将其排序输出到一个临时文件,依此进行,将待排序数据组织为多个有序的临时文件。尔后在归并阶段将这些临时文件组合为一个大的有序文件,也即排序结果。
内排序算法
- 插入排序
- 交换排序
- 选择排序
- 归并排序
- 基数排序
插入类排序
直接插入排序:

现有一组数组 \(arr = [5, 6, 3, 1, 8, 7, 2, 4]\),共有8个记录,排序过程如下:
最好情况下「正序」的时间复杂度:\(O(n)\),比较次数为\(n-1\)次,移动次数\(2(n-1)\)次
最坏情况「逆序」下的时间复杂度:\(O(n^2)\),比较次数\(\sum_{i=1}^{n-1}i\),移动次数\(\sum_{i=1}^{n-1}(i+2)\)
平均时间复杂度:\(O(n^2)\)
空间复杂度:\(O(1)\)
不能保证一趟之后有一个元素在其最终的位置
是稳定的排序算法
折半插入排序:

注意后面的第二个for循环中 \(j >= high + 1\),而不是\( j >= high \)
从时间上看,折半插入排序只是减少了关键字间的比较次数,而元素的移动次数不变,因为找到位置之后还是要将元素全部移动,因此平均时间复杂度:\(O(n^2)\)
空间复杂度:\(O(1)\)
\(Shell\)排序: 又称为缩小增量排序方法,其基本思想是:把记录按下标的某个增量\(d\)分组,对每组记录采用直接插入排序方法进行排序,随着增量逐渐缩小,所分成的组所包含的记录越来越多,到增量的值减少到\(1\)时,整个数据合成为一组,构成一组有序记录,则完成排序。
- 先取一个正整数 \(d_1(d_1 < n)\),把全部记录分成 \(d_1\) 个组,所有距离为 \(d_1\) 的倍数的记录看成一组,然后在各组内进行插入排序
然后取 \(d_2(d_2 < d_1)\)重复上述分组和排序操作;直到取 \(d_i = 1(i >= 1)\) 位置,即所有记录成为一个组,最后对这个组进行插入排序。一般选 \(d_1 约为 n/2,d_2 为 d_1 /2, d_3 为 d_2/2 ,…, d_i = 1\)。



\(shell\)排序的每趟排序,都会使得整个序列变得更加有序,等整个序列基本有序了,再来一趟直接插入排序,这样会使排序效率更高
不能保证一趟之后有一个元素在其最终的位置
\(shell\)排序是 不稳定的排序算法,例如\(2,2,1(d=2,1)\)
分析\(shell\)排序是一个复杂的问题, 它的时间复杂度是"增量“序列的函数,到现在为止还未得到数学上的解决

空间复杂度为\(O(1)\)
交换类排序
大数沉底的冒泡排序的基本流程
比较相邻的元素。如果第一个比第二个大,就交换他们两个。
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。交换到最后的时候,最后的一个元素将会是最大的数
针对所有的元素重复以上的步骤,除了刚刚找到的最后一个
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较


最坏情况:待排序序列逆序,时间复杂度为\(O(n^2)\)
最好情况:待排序序列有序,时间复杂度为\(O(n)\)
平均时间复杂度为:\(O(n^2)\)
空间复杂度为\(O(1)\)
每次都能保证一个元素在最终位置
是稳定的排序算法
快速排序:它采用了一种分治的策略,将原问题分解为若干个规模更小但结构与原问题相似的子问题。递归地解这些子问题,然后将这些子问题的解组合为原问题的解。
在数据集之中,选择一个元素作为"基准"\((pivot)\)
所有小于"基准"的元素,都移到"基准"的左边;所有大于"基准"的元素,都移到"基准"的右边
对"基准"左边和右边的两个子集,不断重复第一步和第二步,直到所有子集只剩下一个元素为止
假设数据集为\({85, 24, 63, 45, 17, 31, 96, 50}\),怎么对其排序呢







上面这种方法应该注意后面是small++之后再交换,想象一下全部有序就能理解了😂快速排序最好情况下的时间复杂度为\(O(nlog_2n)\)
最坏情况下的时间复杂度为\(O(n^2)\)
平均时间复杂度为\(O(nlog_2n)\),就平均而言快速排序是所有排序算法中效果最好的, 这是快排的名字的由来
待排序序列越接近无序,算法效率越高,个人理解应该是无序的时候基点两边平衡的比较好,所以效率比较高。
待排序序列越接近有序,算法效率越低(快速排序反而蜕化为冒泡排序?)
快速排序的趟数和初始序列相关
不稳定的排序算法\(2,2,1\)
算法的空间复杂度为\(O(log_2n)\)
选择类排序
简单选择排序:每一趟在\(n-i(i=1,2,...,n-1)\)个数据元素中\(R[i],R[i+1],...,R[n-1]\)中选择最小的数据元素作为有序序列中第\(i\)个数据元素。

不稳定的排序算法:\(2,2,1\)
时间复杂度为\(O(n^2)\)
空间复杂度为\(O(1)\)
堆排序:可以把堆 看成一棵完全二叉树,满足:任何一个非叶子节点的值都不大于(或不小于)其左右孩子节点的值,若父亲大孩子小,则这样的堆叫做大顶堆,若父亲小孩子大,则这样的堆叫做小顶堆。
根据堆的定义,代表堆的这棵完全二叉树的根节点的值是最大的或者最小的,因此将一个无序的序列调整为一个堆,就可以找出这个序列的最大(最小)值,然后将找出的这个值交换到序列的最后(或最前),这样有序序列元素增加1个,无序序列中元素减少1个,对新的无序序列重复这样的操作就可以实现排序。


在判断R[i]<R[i+1]的时候,别忘记了要判断i< high



平均时间复杂度为\(O(nlog_2(n))\)
最坏情况下的时间复杂度也为\(O(nlog_2(n))\)
空间复杂度为\(O(1)\), 空间复杂度是指占用内存大小,每次调用完调整堆的的函数后,所占用的那一个空间都会被释放,不想快速排序那样未排完之前是不会释放辅助空间的
适用于记录数较多的情况
是不稳定排序方法\(2,2,1\)(小顶堆)
归并排序
\(2\)路归并排序:将两个有序的子序列合并成一个新的有序子序列。归并的思想:将序列看成\(n\)个有序子序列,将序列看成是\(n\)个有序子序列,每个序列的长度为\(1\),然后两两归并,得到\(⌈\frac{n}{2}⌉\)个长度为\(2\),的有序子序列,然后两两归并....,如此重复,直到得到一个长度为\(n\)的有序子序列



排序时间代价不依赖于待排序数组的初始情况
最好,最坏,平均时间复杂度都为\(O(nlog_2n)\)
空间复杂度为\(O(n)\)
稳定的排序方法
在平均情况下还是快速排序最快(常数因子更小)
基数排序
将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。基数排序法会使用到桶 \((Bucket),先进先出的队列\),顾名思义,通过将要比较的位(个位、十位、百位…),将要排序的元素分配至 \(0 \to 9\) 个桶中,借以达到排序的作用,在某些时候,基数排序法的效率高于其它的比较性排序法。

是稳定的排序方法
时间复杂度:设数组长度为\(n\),基数为\(r\),关键字位数为\(d\),则每趟分配的时间为\(O(n)\),每趟收集的时间复杂度为\(O(n)\),工序\(d\)趟分配与收集,所以时间复杂度为\(O(d(2n))\),即\(O(d \times n)\)
空间复杂度为\(O(rn)\)

外部排序
所谓外排序,顾名思义,即是在内存外面的排序,因为当要处理的数据量很大,而不能一次装入内存时,此时只能放在读写较慢的外存储器(通常是硬盘)上。外排序通常采用的是一种“排序-归并”的策略。
- 在排序阶段,先读入能放在内存中的数据量,将其排序输出到一个临时文件,依此进行,将待排序数据组织为多个有序的临时文件;
- 尔后在归并阶段将这些临时文件组合为一个大的有序文件,也即排序结果。
例子:假定现在有\(20\)个数据的文件\(A:{5,11, 0, 18, 4, 14, 9, 7 6, 8, 12, 17 ,16 ,13 ,19 ,10, 2, 1, 3, 15}\),但一次只能使用仅装\(4\)个数据的内容,所以,我们可以每趟
对\(4\)个数据进行排序,即\(5\)路归并,具体方法如下述步骤:- 我们先把“大”文件\(A\),分割为\(a_1,a_2,a_3,a_4,a_5等5\)个小文件,每个小文件\(4\)个数据
- \(a_1文件为:5,11, 0,18\)
- \(a_2文件为:4,14,9,7\)
- \(a_3文件为:6, 8, 12, 17\)
- \(a_4文件为:16,13, 19,10\)
- \(a_5文件为:2,1,3,15\)然后依次对\(5\)个小文件分别进行排序
- \(a_1文件完成排序后:0,5,11,18\)
- \(a_2文件完成排序后:4,7,9,14\)
- \(a_3文件完成排序后:6,8,12,17\)
- \(a_4文件完成排序后:10,13,16,19\)
- \(a_5文件完成排序后:1,2,3,15\)最终多路归并,完成整个排序 整个大文件\(A\)文件完成排序后:\(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19\)
例子:要对\(900MB\)的数据进行排序,但机器上只有\(100MB\)的可用内存时,外部归并排序按如下方法操作:
- ①读入\(100MB\)的数据至内存中,用某种常规排序(如:快速排序、堆排序、归并排序等)在内存中完成排序。
- ②将排序完成的数据写入磁盘(临时文件)。
- ③重复步骤\(1\)和步骤\(2\)直到所有的数据都存入了不同的\(100MB\)的块(临时文件)中。本例中,\(900MB数据,100MB\)内存,故产生了9个临时文件。
- ④读入每个临时文件(顺串)的前\(10MB(=100MB/(9块+1)\),在进行多路归并的时候,这里有\(9\)路,然后输出缓冲算一个,所以要将内存资源分给\(10\)个部分)的数据放入内存中的输入缓冲区(总计\(90MB\)),最后的\(10MB\)作为输出缓冲区。(实践中,将输入缓冲适当调小,而适当增大输出缓冲区能获得比较好的效果)
- ⑤执行\(9\)路归并算法,将结果输出到输出缓冲区。一旦输出缓冲区满,将缓冲区中的数据写到目标文件,清空缓冲区。一旦\(9\)个输入缓冲区的一个变空,就从这个缓冲区关联的文件中读入下一个\(10MB\)数据,除非这个文件已读完。这是“外部归并排序”能在主存外完成排序的关键步骤——因为“归并算法”对每一个大块只是顺序地做一轮访问(进行归并),每个大块不用完全载入主存。
- \(1-3\)步是排序,\(4-5\)步是归并。
- 为了增加每一个有序的临时文件的长度,可以采用置换选择排序。它可以产生大于内存的顺串。具体方法是在内存中使用一个最小堆进行排序,设这个最小堆的大小为\(M\),算法描述如下:
- ①初始时将输入文件读入内存,建立最小堆。
- ②将堆顶元素输出至输出缓冲区。然后读入下一个记录。
- 2.1若该元素的关键码值不小于刚输出的关键码值,将其作为堆顶元素并调整堆,使之满足堆的性质。
- 2.2若该元素的关键码值小于刚输出的关键码值,将新元素放入堆底位置,将堆的大小减\(1\)。
- ③重复第②步,直至堆大小变为\(0\)。
- ④此时一个顺串已经产生。将堆中的所有元素建堆,开始生成下一个顺串。




桶排序
桶排序\((Bucket \ Sort)\)的基本思路是:
将待排序元素划分到不同的桶。先扫描一遍序列求出最大值\(max\)和最小值\(min\),设桶的个数为\(k\),则把区间\([min, max]\)均匀划分成\(k\)个区间,每个区间就是一个桶。将序列中的元素分配到各自的桶。
对每个桶内的元素进行排序。可以选择任意一种排序算法。
将各个桶中的元素合并成一个大的有序序列。
假设数据是均匀分布的,则每个桶的元素平均个数为\(\frac{n}{k}\)。
假设选择用快速排序对每个桶内的元素进行排序,那么每次排序的时间复杂度为\(O(\frac{n}{k}log(\frac{n}{k}))\)。
总的时间复杂度为\(O(n)+O(m)O(\frac{n}{k}log(\frac{n}{k}))=O(n+nlog(\frac{n}{k}))=O(n+nlogn-nlogk)\)。当 \(k\) 接近于 \(n\) 时,桶排序的时间复杂度就可以近似认为是 \(O(n)\) 的。即桶越多,时间效率就越高,而桶越多,空间就越大。
计数排序
计数排序(Counting sort)是一种稳定的线性时间排序算法。计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。当输入的元素是n个0到k之间的整数时,它的运行时间是Θ(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存