一、排序的基本概念
1.1 排序的定义
排序:将表中的元素重新排列,使其按关键字有序的过程。
稳定性 :若关键字相同的元素在排序前后相对顺序不变,则称该排序算法是稳定的 ;否则为不稳定的。
稳定性不能衡量算法优劣,只是描述其性质。当关键字互不重复时,稳定性无关紧要。
1.2 内部排序 vs 外部排序
| 类型 | 定义 |
|---|---|
| 内部排序 | 排序期间所有元素都存放在内存中 |
| 外部排序 | 元素无法全部装入内存,需在内、外存之间频繁交换数据 |
1.3 排序算法的分类
按基本思想分为五大类:
-
插入排序:直接插入排序、折半插入排序、希尔排序
-
交换排序:冒泡排序、快速排序
-
选择排序:简单选择排序、堆排序
-
归并排序
-
基数排序
大多数内部排序算法更适用于顺序存储的线性表。
二、插入排序
2.1 直接插入排序
核心思想:每次将一个待排序的记录,按其关键字大小插入到前面已经排好序的子序列中。
算法步骤(对L[2]到L[n]循环):
-
查找L[i]在L[1..i-1]中的插入位置k
-
将L[k..i-1]中的所有元素后移一位
-
将L[i]放入位置k
性能分析:
| 指标 | 值 |
|---|---|
| 空间复杂度 | O(1) |
| 最好时间复杂度 | O(n)(已有序) |
| 最坏时间复杂度 | O(n²)(逆序) |
| 平均时间复杂度 | O(n²) |
| 稳定性 | 稳定 |
| 适用性 | 顺序存储 + 链式存储 |
哨兵:A[0]用作哨兵,避免循环中反复判断数组下标是否越界。
2.2 折半插入排序
改进点:将"边比较边移动"改为"先折半查找确定插入位置,再统一移动元素"。
性能分析:
| 指标 | 值 |
|---|---|
| 空间复杂度 | O(1) |
| 比较次数 | O(n log₂n) |
| 移动次数 | 仍取决于初始序列,最坏O(n²) |
| 时间复杂度 | O(n²) |
| 稳定性 | 稳定 |
| 适用性 | 仅顺序存储 |
折半插入排序减少了比较次数 ,但移动次数未减少,时间复杂度仍为O(n²)。
2.3 希尔排序(缩小增量排序)
核心思想:先取一个小于n的增量d₁,将表分成d₁个子序列,对各子序列进行直接插入排序;然后取更小的增量d₂,重复;直到d=1。
增量序列要求 :d₁ > d₂ > ... > d_k = 1
例:初始序列(8 个元素):
49, 38, 65, 97, 76, 13, 27, 49
第一趟:d₁ = 4(n/2 = 4)
分成 4 组,每组对"间隔 4"的元素进行直接插入排序:
| 子序列(下标从1开始) | 排序前 | 排序后 |
|---|---|---|
| 第1组(1,5) | 49, 76 | 49, 76 |
| 第2组(2,6) | 38, 13 | 13, 38 |
| 第3组(3,7) | 65, 27 | 27, 65 |
| 第4组(4,8) | 97, 49 | 49, 97 |
第一趟结果:
49, 13, 27, 49, 76, 38, 65, 97
第二趟:d₂ = 2
分成 2 组,对"间隔 2"的元素直接插入排序:
| 子序列 | 排序前 | 排序后 |
|---|---|---|
| 第1组(奇数位:1,3,5,7) | 49, 27, 76, 65 | 27, 49, 65, 76 |
| 第2组(偶数位:2,4,6,8) | 13, 49, 38, 97 | 13, 38, 49, 97 |
第二趟结果:
27, 13, 49, 38, 65, 49, 76, 97
第三趟:d₃ = 1
整体直接插入排序(过程略)
最终结果:
13, 27, 38, 49, 49, 65, 76, 97
性能分析:
| 指标 | 值 |
|---|---|
| 空间复杂度 | O(1) |
| 时间复杂度 | 约O(n^1.3),最坏O(n²) |
| 稳定性 | 不稳定(相同关键字可能分到不同子表) |
| 适用性 | 仅顺序存储 |
希尔排序的增量序列如何选择是最优的,目前仍是数学上的未解难题。
三、交换排序
3.1 冒泡排序
核心思想:从后往前(或从前往后)依次比较相邻元素,若逆序则交换。每一趟都将当前未排序部分的最小(或最大)元素放到其最终位置。
优化:设置flag标志,若某一趟没有发生交换,说明序列已有序,可提前终止。
性能分析:
| 指标 | 值 |
|---|---|
| 空间复杂度 | O(1) |
| 最好时间复杂度 | O(n)(已有序) |
| 最坏时间复杂度 | O(n²)(逆序) |
| 平均时间复杂度 | O(n²) |
| 稳定性 | 稳定 |
| 适用性 | 顺序存储 + 链式存储 |
重要性质 :冒泡排序每趟产生的有序子序列具有全局有序性(已排序部分的所有元素均小于未排序部分的所有元素)。
3.2 快速排序
核心思想 :任取一个元素作为枢轴(pivot),通过一趟划分将表分成两部分,使左边所有元素小于枢轴,右边所有元素大于等于枢轴。然后递归地对两个子表进行相同操作。
一趟划分过程(以首元素为枢轴):
初始:49,38,65,97,76,13,27,49
i→ ←j
取pivot=49
j从右向左找<49 → 找到27,放入i位置:27 ,38,65,97,76,13,27 ,49
i从左向右找>49 → 找到65,放入j位置:27,38,65 ,97,76,13,65 ,49
j继续向左找<49 → 找到13,放入i位置:27,38,13 ,97,76,13 ,65,49
i继续向右找>49 → 找到97,放入j位置:27,38,13,97 ,76,97 ,65,49
i与j相遇 → 将pivot放入:27,38,13,49,76,97,65,49
第一趟结果:27,38,13,【49】,76,97,65,49
左边<49 右边≥49
(后续排序结果略,即左右两边不断递归排序)
性能分析:
| 指标 | 值 |
|---|---|
| 空间复杂度 | 最好O(log₂n),最坏O(n)(递归栈) |
| 最好时间复杂度 | O(n log₂n)(每次划分均匀) |
| 最坏时间复杂度 | O(n²)(基本有序或逆序) |
| 平均时间复杂度 | O(n log₂n) |
| 稳定性 | 不稳定 |
| 适用性 | 仅顺序存储 |
重要 :快速排序是所有内部排序算法中平均性能最优的排序方法。
改进划分平衡性的方法:
-
三数取中法:从首、尾、中间三个位置取中位数作为枢轴
-
随机选取枢轴
性质 :每趟划分能确保枢轴元素被放到其最终位置。
四、选择排序
4.1 简单选择排序
核心思想:每一趟从待排序元素中选出关键字最小的元素,放到已排序序列的末尾。
性能分析:
| 指标 | 值 |
|---|---|
| 空间复杂度 | O(1) |
| 比较次数 | 始终为 n(n-1)/2 |
| 移动次数 | 最多3(n-1)次,最好0次 |
| 时间复杂度 | O(n²)(与初始序列无关) |
| 稳定性 | 不稳定 |
| 适用性 | 顺序存储 + 链式存储 |
简单选择排序的比较次数与初始序列无关,总比较次数恒为n(n-1)/2。
4.2 堆排序
4.2.1 堆的定义
堆:n个关键字序列L[1..n]满足:
大根堆 :
L[i] ≥ L[2i]且L[i] ≥ L[2i+1](最大元素在根)小根堆 :
L[i] ≤ L[2i]且L[i] ≤ L[2i+1](最小元素在根)
堆可以视为一棵完全二叉树。
4.2.2 堆排序的基本思路
- 将待排序序列构建成初始堆(大根堆)
- 交换堆顶元素与堆底元素,最大值归位
- 将剩余n-1个元素重新调整为堆
- 重复2-3,直到堆中只剩一个元素
4.2.3 建堆过程(自底向上)
从最后一个分支结点 (编号⌊n/2⌋)开始,依次向前处理到根结点,对每个结点执行向下调整:若该结点小于其左右孩子中的较大者,则交换,并继续向下调整,直到满足堆性质。
初始序列:53,17,78,09,45,65,87,32
最后一个分支结点:i=4 → 09<32,交换
i=3 → 78<87,交换
i=2 → 17<45,交换
i=1 → 53<87,交换 → 破坏L(3)子树 → 53<78,交换
建堆完成
(1)初始序列(看作完全二叉树)
序列:53, 17, 78, 09, 45, 65, 87, 32
对应的完全二叉树(编号从1开始):
1(53)
/ \
2(17) 3(78)
/ \ / \
4(09) 5(45) 6(65) 7(87)
/
8(32)
最后一个分支结点编号 = ⌊8/2⌋ = 4,即结点4(09)。
(2)第一步:调整结点4(09)
结点4(09)的左右孩子:左=8(32),右=无。
4(09)
/
8(32)
09 < 32 → 交换。
调整后:
4(32)
/
8(09)
整棵树变为:
1(53)
/ \
2(17) 3(78)
/ \ / \
4(32) 5(45) 6(65) 7(87)
/
8(09)
(3)第二步:调整结点3(78)
结点3(78)的左右孩子:左=6(65),右=7(87)。
较大孩子是7(87),78 < 87 → 交换。
3(87)
/ \
6(65) 7(78)
整棵树变为:
1(53)
/ \
2(17) 3(87)
/ \ / \
4(32) 5(45) 6(65) 7(78)
/
8(09)
(4)第三步:调整结点2(17)
结点2(17)的左右孩子:左=4(32),右=5(45)。
较大孩子是5(45),17 < 45 → 交换。
2(45)
/ \
4(32) 5(17)
整棵树变为:
1(53)
/ \
2(45) 3(87)
/ \ / \
4(32) 5(17) 6(65) 7(78)
/
8(09)
(5)第四步:调整结点1(53)------这里会发生"破坏"
结点1(53)的左右孩子:左=2(45),右=3(87)。
较大孩子是3(87),53 < 87 → 交换。
1(87)
/ \
2(45) 3(53)
此时整棵树变为:
1(87)
/ \
2(45) 3(53) ← 53被换下来了
/ \ / \
4(32) 5(17) 6(65) 7(78)
/
8(09)
(6)"破坏L(3)子树"的意思
原来结点3(78)已经调整好了(78的子树满足堆性质)。
但当把53换到结点3的位置后:
3(53)
/ \
6(65) 7(78)
53 < 65 ?是,53 < 65。
53 < 78?是,53 < 78。
但是最大堆要求 :父结点 ≥ 子结点。
这里 53 比左右孩子都小 → 堆性质被破坏了。
(7)继续向下调整:对结点3(53)再次执行"下沉"
3(53)
/ \
6(65) 7(78)
较大孩子是7(78),53 < 78 → 交换。
3(78)
/ \
6(65) 7(53)
整棵树最终变为:
1(87)
/ \
2(45) 3(78)
/ \ / \
4(32) 5(17) 6(65) 7(53)
/
8(09)
(8)最终大根堆
87
/ \
45 78
/ \ / \
32 17 65 53
/
09
建堆时间复杂度:O(n)
4.2.4 堆的调整(向下筛选)
void HeapAdjust(ElemType A[], int k, int len) {
A[0] = A[k];
for (int i = 2 * k; i <= len; i *= 2) {
if (i < len && A[i] < A[i + 1]) i++; // 取较大孩子
if (A[0] >= A[i]) break;
else {
A[k] = A[i];
k = i;
}
}
A[k] = A[0];
}
堆调整的时间复杂度:O(log₂n)
4.2.5 堆排序的完整过程
- BuildMaxHeap(A, len) // 建堆,O(n)
- for (i = len; i > 1; i--)
{
Swap(A[1], A[i]); // 输出堆顶到末尾
HeapAdjust(A, 1, i-1); // 调整剩余元素
}
4.2.6 堆的插入操作
将新元素放在堆的末端,然后自下而上与父结点比较,若违反堆性质则交换,直至满足堆定义。
4.2.7 堆的应用------Top-K问题
在1亿个数中找出最大的100个数:
读入前100个数,构建小顶堆
依次读入剩余数字,若小于堆顶则舍弃,否则替换堆顶并向下调整
读取完毕后,堆中的100个数即为所求
时间复杂度:O(n log₂k),空间复杂度:O(1)
4.2.8 堆排序性能分析
| 指标 | 值 |
|---|---|
| 空间复杂度 | O(1) |
| 建堆时间复杂度 | O(n) |
| 排序时间复杂度 | O(n log₂n) |
| 总时间复杂度 | O(n log₂n)(与初始序列无关) |
| 稳定性 | 不稳定 |
| 适用性 | 仅顺序存储 |
五、思考
1. 直接插入排序 ≈ 整理扑克牌
手里已经有一堆排好序的牌,新摸一张牌,从右往左找到合适的位置插入。这就是直接插入排序。
2. 希尔排序 ≈ 军训分组训练先按身高间隔5个人分成小组,组内对齐;再按间隔3个人分组,再对齐;最后全体对齐。虽然每次不是最终顺序,但越来越接近有序。
3. 冒泡排序 ≈ 气泡上浮轻的气泡(小元素)一路向上"冒",重的石头(大元素)一路向下"沉"。每一趟都会有一个元素到达最终位置。
4. 快速排序 ≈ 班级选人选一个"基准"同学,比他矮的站左边,比他高的站右边。然后左右两组各自重复这个过程。每次都能确定一个人的最终位置。
5. 堆排序 ≈ 锦标赛淘汰赛先通过小组赛(建堆)找出冠军(堆顶),然后把冠军拿走,剩下的选手重新比赛找出新冠军。每次都能选出当前最大(或最小)。
六、各算法复杂度表
| 算法 | 时间复杂度(最好) | 时间复杂度(最坏) | 时间复杂度(平均) | 空间复杂度 | 稳定性 |
|---|---|---|---|---|---|
| 直接插入排序 | O(n) | O(n²) | O(n²) | O(1) | 稳定 |
| 折半插入排序 | O(n log₂n) | O(n²) | O(n²) | O(1) | 稳定 |
| 希尔排序 | O(n^1.3) | O(n²) | O(n^1.3) | O(1) | 不稳定 |
| 冒泡排序 | O(n) | O(n²) | O(n²) | O(1) | 稳定 |
| 快速排序 | O(n log₂n) | O(n²) | O(n log₂n) | O(log₂n)~O(n) | 不稳定 |
| 简单选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 |
| 堆排序 | O(n log₂n) | O(n log₂n) | O(n log₂n) | O(1) | 不稳定 |
注:以上内容参考 2027年数据结构考研复习指导 王道论坛 组编,其中有一些个人想法,如有任何错误或不妥,欢迎各位大佬指出,如果各位有一些有意思的想法,也可以和我交流一下~感谢!
七、明日计划
排序(下)