返回目录

§9 排序

§9.1 排序的基本概念

在排序过程中,若整个表都是放在内存中处理,排序时不涉及数据的内、外存交换,则称之为 内排序。根据内排序算法是否 基于关键字的比较,将内排序算法分为 基于比较的排序算法 和不基于比较的排序算法。

  • 基数排序是典型的不基于比较的排序算法。
  • 基于比较的排序算法中,主要进行 比较移动 这两种基本操作,这类排序算法的性能由算法的 时间和空间 确定的,而时间是由比较和移动的次数之和确定的,例如,两个记录的一次交换一般需要三次移动。

若排序过程中要进行 数据的内、外存交换,则称之为 外排序

如果待排序的表中,存在有多个关键字相同的记录,经过排序后这些具有 相同关键字 的记录之间的 相对次序保持不变,称这种排序方法是 稳定 的。希尔、快排、选择排序是不稳定的(重要)。

§9.2 插入排序

§9.2.1 直接插入

每一趟将一个待排序的元素,按大小 插入到已经排序中 的适当位置上。

时间 $\Omega(n)-\Theta(n^{2})-\mathrm{O}(n^{2})$,空间 $\Omega(1)-\Theta(1)-\mathrm{O}(1)$,稳定

§9.2.2 折半插入

优化了个寂寞,其瓶颈主要是顺序表的移动。

时间 $\Omega(n)-\Theta(n^{2})-\mathrm{O}(n^{2})$,空间 $\Omega(1)-\Theta(1)-\mathrm{O}(1)$,稳定

§9.2.3 希尔排序(不太重要)

基于插入排序,将待排序数组 分割成多个子序列,这些子序列的元素间隔一定步长,然后 对每个子序列进行插入排序,随着 步长的逐渐减小,最终整个数组变为有序。

时间 $\Omega(n\log n)-\Theta(n^{1.3})-\mathrm{O}(n^{2})$,空间 $\Omega(1)-\Theta(1)-\mathrm{O}(1)$,不稳定

§9.3 交换排序

§9.3.1 冒泡排序

倒序遍历,如果相邻两项不是有序的,就交换之,重复多轮。

时间 $\Omega(n)-\Theta(n^{2})-\mathrm{O}(n^{2})$,空间 $\Omega(1)-\Theta(1)-\mathrm{O}(1)$,稳定

§9.3.2 快速排序

选择一个基准元素,通过一趟排序将待排序的数组分割成独立的两部分,其中一部分的所有元素都不大于另一部分的所有元素,然后递归地对这两部分继续进行快速排序,直至整个数组有序。快速排序在被排序的数组随机分布下最容易发挥其长处。

时间 $\Omega(n\log n)-\Theta(n\log n)-\mathrm{O}(n^{2})$,空间 $\Omega(\log n)-\Theta(\log n)-\mathrm{O}(\log n)$,不稳定

提醒:未经任何优化的快排,最坏时间是 $\mathrm{O}(n^{2})$。std 中的快排是优化过的,保证上界严格不超过 $\mathrm{O}(n\log n)$。

§9.4 选择排序

§9.4.1 简单选择排序

每步从待排序的记录中选出关键字最小的记录,按顺序放在已排序的记录序列的最后,直到全部排完为止。

时间 $\Omega(n^{2})-\Theta(n^{2})-\mathrm{O}(n^{2})$,空间 $\Omega(1)-\Theta(1)-\mathrm{O}(1)$,不稳定

§9.4.2 堆排序(不太重要)

大概思路掌握就行,具体细节不重要,其实作者也没看懂

基于比较,利用堆这种数据结构的特性,首先将待排序数组构建成一个 大根堆,使得每个父节点的值都大于或等于其子节点的值;然后通过反复 移除堆顶元素(最大值),将其与数组末尾元素交换,并重新 调整 堆以保持大根堆的性质,直到堆中只剩下一个元素,此时整个数组已经按照升序排列完成。

时间 $\Omega(n\log n)-\Theta(n\log n)-\mathrm{O}(n\log n)$,空间 $\Omega(1)-\Theta(1)-\mathrm{O}(1)$,不稳定

§9.5 归并排序

多次将两个或两个以上的有序表合并成一个新的有序表,最简单的归并是直接将两个有序的子表合并成一个有序的表——二路归并排序

时间 $\Omega(n\log n)-\Theta(n\log n)-\mathrm{O}(n\log n)$,空间 $\Omega(n)-\Theta(n)-\mathrm{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 基数排序

通过 分配收集 过程来实现排序,而非比较。根据数字的位数(如个位、十位、百位等)将数组分成不同的桶,然后在每一轮中,根据当前轮次的位数对所有桶中的元素进行收集和分配,使得相同位数的数字被放在一起,通过这种方式,从最低位到最高位逐轮进行,直到整个数组按照升序或降序排列完成。

时间 $\mathrm{O}(d(n+r))$,其中分配 $\mathrm{O}(n)$,收集 $\mathrm{O}(r)$,$r$ 是基数,$d$ 是收集后分配的趟数,空间 $\Omega(r)-\Theta(r)-\mathrm{O}(r)$,稳定

对于整数,这是少有的线性排序方法。