一、归并排序
1.1 基本思想
归并排序:将两个或多个有序序列合并为一个更长的有序序列。2路归并排序是其中最常用的形式。
2路归并排序过程:
初始: [49] [38] [65] [97] [76] [13] [27]
第一趟: [38 49] [65 97] [13 76] [27]
第二趟: [38 49 65 97] [13 27 76]
第三趟: [13 27 38 49 65 76 97]
1.2 归并操作(Merge)
ElemType *B = (ElemType *)malloc((n + 1) * sizeof(ElemType));
void Merge(ElemType A[], int low, int mid, int high) {
for (int k = low; k <= high; k++) B[k] = A[k];
int i = low, j = mid + 1, k = low;
while (i <= mid && j <= high) {
if (B[i] <= B[j]) A[k++] = B[i++];
else A[k++] = B[j++];
}
while (i <= mid) A[k++] = B[i++];
while (j <= high) A[k++] = B[j++];
}
1.3 递归实现
void MergeSort(ElemType A[], int low, int high) {
if (low < high) {
int mid = (low + high) / 2;
MergeSort(A, low, mid);
MergeSort(A, mid + 1, high);
Merge(A, low, mid, high);
}
}
1.4 性能分析
| 指标 | 值 |
|---|---|
| 空间复杂度 | O(n)(需辅助数组) |
| 时间复杂度(最好/最坏/平均) | O(n log₂n) |
| 稳定性 | 稳定 |
| 适用性 | 顺序存储 + 链式存储 |
每趟归并需遍历所有元素O(n),共⌈log₂n⌉趟,总时间复杂度O(n log₂n)。
二、基数排序
2.1 基本思想
基数排序 :不比较元素大小,而是基于关键字各位的大小进行排序。是一种多关键字排序。
两种方法:
-
MSD(最高位优先):按关键字位权重递减,逐层划分
-
LSD(最低位优先):按关键字位权重递增,依次排序(更常用)
2.2 链式基数排序(LSD)
以10个三位数为例:278, 109, 063, 930, 589, 184, 505, 269, 008, 083
第一趟(按个位分配→收集):
分配:0:930 3:063,083 4:184 5:505 8:278,008 9:109,589,269
收集:930,063,083,184,505,278,008,109,589,269
第二趟(按十位分配→收集):
分配:0:505,008,109 6:063,269 7:278 8:083,184 9:589,930
收集:505,008,109,063,269,278,083,184,589,930
第三趟(按百位分配→收集):
分配:0:008,063,083 1:109,184 2:269,278 5:505,589 9:930
收集:008,063,083,109,184,269,278,505,589,930
2.3 性能分析
| 指标 | 值 |
|---|---|
| 空间复杂度 | O(r)(r个队列) |
| 时间复杂度 | O(d(n + r)) |
| 稳定性 | 稳定 |
| 适用性 | 顺序存储 + 链式存储 |
基数排序的时间复杂度与序列初始状态无关。
三、计数排序
3.1 基本思想
计数排序:统计每个元素值出现的次数,根据次数确定每个元素在有序序列中的位置。
void CountSort(ElemType A[], ElemType B[], int n, int k) {
int C[k];
for (int i = 0; i < k; i++) C[i] = 0;
for (int i = 0; i < n; i++) C[A[i]]++;
for (int i = 1; i < k; i++) C[i] = C[i] + C[i - 1];
for (int i = n - 1; i >= 0; i--) {
B[C[A[i]] - 1] = A[i];
C[A[i]]--;
}
}
3.2 性能分析
| 指标 | 值 |
|---|---|
| 空间复杂度 | O(n + k) |
| 时间复杂度 | O(n + k) |
| 稳定性 | 稳定 |
| 适用性 | 顺序存储 |
注 :计数排序不在统考大纲范围内,但其思想在历年真题中多次涉及。
四、各种内部排序算法的比较
4.1 时间复杂度对比
| 算法 | 最好 | 平均 | 最坏 |
|---|---|---|---|
| 直接插入排序 | O(n) | O(n²) | O(n²) |
| 冒泡排序 | O(n) | O(n²) | O(n²) |
| 简单选择排序 | O(n²) | O(n²) | O(n²) |
| 希尔排序 | --- | O(n^1.3) | O(n²) |
| 快速排序 | O(n log₂n) | O(n log₂n) | O(n²) |
| 堆排序 | O(n log₂n) | O(n log₂n) | O(n log₂n) |
| 归并排序 | O(n log₂n) | O(n log₂n) | O(n log₂n) |
| 基数排序 | O(d(n+r)) | O(d(n+r)) | O(d(n+r)) |
4.2 空间复杂度对比
| 算法 | 空间复杂度 |
|---|---|
| 直接插入/冒泡/简单选择/希尔/堆排序 | O(1) |
| 快速排序 | O(log₂n) ~ O(n) |
| 归并排序 | O(n) |
| 基数排序 | O(r) |
4.3 稳定性对比
| 稳定 | 不稳定 |
|---|---|
| 直接插入排序 | 简单选择排序 |
| 冒泡排序 | 快速排序 |
| 归并排序 | 希尔排序 |
| 基数排序 | 堆排序 |
在平均时间复杂度为O(n log₂n)的排序算法中,只有归并排序是稳定的。
4.4 适用性对比
| 适用顺序存储 | 顺序存储 + 链式存储 |
|---|---|
| 折半插入排序 | 直接插入排序 |
| 希尔排序 | 冒泡排序 |
| 快速排序 | 简单选择排序 |
| 堆排序 | 归并排序 |
| --- | 基数排序 |
4.5 排序算法选型建议
| 场景 | 推荐算法 |
|---|---|
| n较小 | 直接插入排序 / 简单选择排序 |
| n较大 | 快速排序 / 堆排序 / 归并排序 |
| 基本有序 | 直接插入排序 / 冒泡排序 |
| 关键字位数少、可分解 | 基数排序 |
| 要求稳定 | 归并排序(O(n log₂n)中唯一稳定) |
| 记录信息量大 | 链表 + 插入类/归并类排序 |
五、外部排序
5.1 基本概念
外部排序:待排序记录存储在外存中,无法一次性全部装入内存,需在内存与外存之间多次交换数据。
外部排序时间组成:
总时间 = 内部排序时间 + 外存读/写时间 + 内部归并时间
外存读/写时间远大于内部排序/归并时间,是优化重点。
5.2 外部排序的基本方法
两个阶段:
-
生成初始归并段:将文件分块读入内存,用内部排序排序后写回外存
-
多路归并:逐趟归并初始归并段,直到得到完整有序文件
示意图(2路归并,8个初始归并段):
R1 R2 R3 R4 R5 R6 R7 R8
↓ ↓ ↓ ↓
R1' R2' R3' R4'
↓ ↓
R1" R2"
↓
R1"'
5.3 多路平衡归并与败者树
问题:增大归并路数k可减少归并趟数,但内部归并时间会增加(每选一个最小元素需k-1次比较)。
解决方案 :引入败者树
败者树特点:
-
k个叶结点存放k个归并段当前参与比较的元素
-
内部结点记录"败者"(关键字较大者)
-
胜者继续向上比较
-
最终胜者存于ls[0]
使用败者树后,每选一个最小元素仅需⌈log₂k⌉次比较,内部归并比较次数与k无关。
归并路数k的限制:k不能过大,否则每个输入缓冲区容量减小,每趟归并中外存读/写次数增加,可能反而降低性能。
5.4 置换-选择排序(生成初始归并段)
目的:生成更长的初始归并段,减少归并段个数r。
算法步骤:
-
从输入文件FI读入w个记录到工作区WA
-
从WA中选出关键字最小的记录,记为MINIMAX
-
将MINIMAX输出到输出文件FO
-
若FI非空,读入下一个记录到WA
-
从WA中所有关键字≥当前MINIMAX的记录中,选出最小者作为新的MINIMAX
-
重复3-5,直到WA中无关键字≥当前MINIMAX的记录(完成一个归并段)
-
重复2-6,直到FI和WA均为空
5.5 最佳归并树
问题:初始归并段长度不等,如何安排归并顺序使I/O次数最少?
解决方案 :构造k叉哈夫曼树(最佳归并树)
虚段的添加:
-
设初始归并段个数为n₀,归并路数为k
-
若
(n₀ - 1) % (k - 1) = 0,可直接构造严格k叉树 -
否则,需添加
(k - 1) - (n₀ - 1) % (k - 1)个虚段(长度为0)
六、思考
1. 归并排序 ≈ 合并两个有序链表
这和合并两个有序链表的代码思想是一样的。归并排序就是反复做这件事------先把序列拆成单个元素(天然有序),再两两合并。
2. 基数排序 ≈ 图书馆图书排序先按个位数字排(书架号),再按十位数字排(楼层号),最后按百位数字排(馆区号)。每趟排序不影响上一趟的相对顺序。(反之亦可)
3. 败者树 ≈ 锦标赛淘汰赛8个选手比赛,每场记录败者,胜者晋级。最后胜者是冠军,而败者树记录了每场比赛谁输了。
4. 置换-选择排序 ≈ 流水线生产工作区就像一个"蓄水池",不断从上游(输入文件)取新元素,同时向下游(输出文件)送元素,只要新元素不小于刚送走的元素,就继续。
七、各算法复杂度表
| 算法 | 时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|
| 归并排序 | O(n log₂n) | O(n) | 稳定 |
| 基数排序 | O(d(n+r)) | O(r) | 稳定 |
| 计数排序 | O(n+k) | O(n+k) | 稳定 |
| 直接插入排序 | O(n²) | O(1) | 稳定 |
| 冒泡排序 | O(n²) | O(1) | 稳定 |
| 简单选择排序 | O(n²) | O(1) | 不稳定 |
| 希尔排序 | O(n^1.3) | O(1) | 不稳定 |
| 快速排序 | O(n log₂n) | O(log₂n) | 不稳定 |
| 堆排序 | O(n log₂n) | O(1) | 不稳定 |
注:以上内容参考 2027年数据结构考研复习指导 王道论坛 组编,其中有一些个人想法,如有任何错误或不妥,欢迎各位大佬指出,如果各位有一些有意思的想法,也可以和我交流一下~感谢!
另:因计划报考的的学校专业课只考察《计算机网络》《数据结构与算法》,故其他两门计算机专业课不再复习。并且这两门专业课已经复习完毕,所以明日起就开始写题。