返回目录
§9 排序 §9.1 排序的基本概念在排序过程中,若整个表都是放在内存中处理,排序时不涉及数据的内、外存交换,则称之为 内排序 。根据内排序算法是否 基于关键字的比较 ,将内排序算法分为 基于比较的排序算法 和不基于比较的排序算法。
基数排序是典型的不基于比较的排序算法。 基于比较的排序算法中,主要进行 比较 、移动 这两种基本操作,这类排序算法的性能由算法的 时间和空间 确定的,而时间是由比较和移动的次数之和确定的,例如,两个记录的一次交换一般需要三次移动。 若排序过程中要进行 数据的内、外存交换 ,则称之为 外排序 。
如果待排序的表中,存在有多个关键字相同的记录,经过排序后这些具有 相同关键字 的记录之间的 相对次序保持不变 ,称这种排序方法是 稳定 的。希尔、快排、选择排序是不稳定的 (重要)。
§9.2 插入排序 §9.2.1 直接插入每一趟将一个待排序的元素,按大小 插入到已经排序中 的适当位置上。
时间Ω ( n ) − Θ ( n 2 ) − O ( n 2 ) \Omega(n)-\Theta(n^{2})-\mathrm{O}(n^{2}) Ω ( n ) − Θ ( n 2 ) − O ( n 2 ) ,空间Ω ( 1 ) − Θ ( 1 ) − O ( 1 ) \Omega(1)-\Theta(1)-\mathrm{O}(1) Ω ( 1 ) − Θ ( 1 ) − O ( 1 ) ,稳定 。
§9.2.2 折半插入优化了个寂寞,其瓶颈主要是顺序表的移动。
时间Ω ( n ) − Θ ( n 2 ) − O ( n 2 ) \Omega(n)-\Theta(n^{2})-\mathrm{O}(n^{2}) Ω ( n ) − Θ ( n 2 ) − O ( n 2 ) ,空间Ω ( 1 ) − Θ ( 1 ) − O ( 1 ) \Omega(1)-\Theta(1)-\mathrm{O}(1) Ω ( 1 ) − Θ ( 1 ) − O ( 1 ) ,稳定 。
§9.2.3 希尔排序(不太重要)基于插入排序,将待排序数组 分割成多个子序列 ,这些子序列的元素间隔一定步长,然后 对每个子序列进行插入排序 ,随着 步长的逐渐减小 ,最终整个数组变为有序。
时间Ω ( n log n ) − Θ ( n 1.3 ) − O ( n 2 ) \Omega(n\log n)-\Theta(n^{1.3})-\mathrm{O}(n^{2}) Ω ( n log n ) − Θ ( n 1 . 3 ) − O ( n 2 ) ,空间Ω ( 1 ) − Θ ( 1 ) − O ( 1 ) \Omega(1)-\Theta(1)-\mathrm{O}(1) Ω ( 1 ) − Θ ( 1 ) − O ( 1 ) ,不稳定 。
§9.3 交换排序 §9.3.1 冒泡排序倒序遍历,如果相邻两项不是有序的,就交换之,重复多轮。
时间Ω ( n ) − Θ ( n 2 ) − O ( n 2 ) \Omega(n)-\Theta(n^{2})-\mathrm{O}(n^{2}) Ω ( n ) − Θ ( n 2 ) − O ( n 2 ) ,空间Ω ( 1 ) − Θ ( 1 ) − O ( 1 ) \Omega(1)-\Theta(1)-\mathrm{O}(1) Ω ( 1 ) − Θ ( 1 ) − O ( 1 ) ,稳定 。
§9.3.2 快速排序选择一个基准元素,通过一趟排序将待排序的数组分割成独立的两部分,其中一部分的所有元素都不大于另一部分的所有元素,然后递归地对这两部分继续进行快速排序,直至整个数组有序。快速排序在被排序的数组随机分布下最容易发挥其长处。
时间Ω ( n log n ) − Θ ( n log n ) − O ( n 2 ) \Omega(n\log n)-\Theta(n\log n)-\mathrm{O}(n^{2}) Ω ( n log n ) − Θ ( n log n ) − O ( n 2 ) ,空间Ω ( log n ) − Θ ( log n ) − O ( log n ) \Omega(\log n)-\Theta(\log n)-\mathrm{O}(\log n) Ω ( log n ) − Θ ( log n ) − O ( log n ) ,不稳定 。
提醒:未经任何优化的快排,最坏时间是O ( n 2 ) \mathrm{O}(n^{2}) O ( n 2 ) 。std
中的快排是优化过的,保证上界严格不超过O ( n log n ) \mathrm{O}(n\log n) O ( n log n ) 。
§9.4 选择排序 §9.4.1 简单选择排序每步从待排序的记录中选出关键字最小的记录,按顺序放在已排序的记录序列的最后,直到全部排完为止。
时间Ω ( n 2 ) − Θ ( n 2 ) − O ( n 2 ) \Omega(n^{2})-\Theta(n^{2})-\mathrm{O}(n^{2}) Ω ( n 2 ) − Θ ( n 2 ) − O ( n 2 ) ,空间Ω ( 1 ) − Θ ( 1 ) − O ( 1 ) \Omega(1)-\Theta(1)-\mathrm{O}(1) Ω ( 1 ) − Θ ( 1 ) − O ( 1 ) ,不稳定 。
§9.4.2 堆排序(不太重要)大概思路掌握就行,具体细节不重要,其实作者也没看懂
基于比较,利用堆这种数据结构的特性,首先将待排序数组构建成一个 大根堆 ,使得每个父节点的值都大于或等于其子节点的值;然后通过反复 移除堆顶元素 (最大值),将其与数组末尾元素交换,并重新 调整 堆以保持大根堆的性质,直到堆中只剩下一个元素,此时整个数组已经按照升序排列完成。
时间Ω ( n log n ) − Θ ( n log n ) − O ( n log n ) \Omega(n\log n)-\Theta(n\log n)-\mathrm{O}(n\log n) Ω ( n log n ) − Θ ( n log n ) − O ( n log n ) ,空间Ω ( 1 ) − Θ ( 1 ) − O ( 1 ) \Omega(1)-\Theta(1)-\mathrm{O}(1) Ω ( 1 ) − Θ ( 1 ) − O ( 1 ) ,不稳定 。
§9.5 归并排序多次将两个或两个以上的有序表合并成一个新的有序表,最简单的归并是直接将两个有序的子表合并成一个有序的表——二路归并排序 。
时间Ω ( n log n ) − Θ ( n log n ) − O ( n log n ) \Omega(n\log n)-\Theta(n\log n)-\mathrm{O}(n\log n) Ω ( n log n ) − Θ ( n log n ) − O ( n log n ) ,空间Ω ( n ) − Θ ( n ) − O ( n ) \Omega(n)-\Theta(n)-\mathrm{O}(n) Ω ( n ) − Θ ( n ) − O ( n ) ,稳定 。
个人认为是最优雅的排序算法。放个代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ll merge_sort (vector<int >& a) { auto solve = [&](this auto && self, int L, int R) -> ll { if (R - L <= 1 ) return 0ll ; int m = L + R >> 1 ; ll res = self (L, m) + self (m, R); vector<int > b; int l = L, r = m; while (l < m && r < R) { if (a[l] <= a[r]) b.push_back (a[l++]); else b.push_back (a[r++]), res += m - l; } while (l < m) b.push_back (a[l++]); while (r < R) b.push_back (a[r++]); copy (b.begin (), b.end (), a.begin () + L); return res; }; return solve (0 , a.size ()); }
§9.6 基数排序通过 分配 和 收集 过程来实现排序,而非比较 。根据数字的位数(如个位、十位、百位等)将数组分成不同的桶,然后在每一轮中,根据当前轮次的位数对所有桶中的元素进行收集和分配,使得相同位数的数字被放在一起,通过这种方式,从最低位到最高位逐轮进行,直到整个数组按照升序或降序排列完成。
时间O ( d ( n + r ) ) \mathrm{O}(d(n+r)) O ( d ( n + r ) ) ,其中分配O ( n ) \mathrm{O}(n) O ( n ) ,收集O ( r ) \mathrm{O}(r) O ( r ) ,r r r 是基数,d d d 是收集后分配的趟数,空间Ω ( r ) − Θ ( r ) − O ( r ) \Omega(r)-\Theta(r)-\mathrm{O}(r) Ω ( r ) − Θ ( r ) − O ( r ) ,稳定 。
对于整数,这是少有的线性排序方法。