第8章 排序 自学版¶
章节定位:综合收束章
- 这一章把内部排序、非比较型排序和外部排序整合成“过程 + 性质 + 适用场景”的总比较框架。
- 建议先理解每类排序的核心动作,再做稳定性、复杂度和外部排序归并过程的综合对照。
1. 本章定位¶
第 8 章是整本书里“最容易一口气学很多算法名字,但最容易越学越乱”的一章。
表面上它讲的是:
- 直接插入排序
- 冒泡排序
- 快速排序
- 选择排序
- 堆排序
- 归并排序
- 计数排序
- 基数排序
- 外部排序
但真正主线不是“把名字背下来”,而是:
这些排序算法到底是通过什么动作,让序列一步步变得更有序。
如果这条主线不抓住,学生很容易学成:
- 会背时间复杂度
- 会背稳定性
- 但一看到代码就断线
所以这一章我们会始终盯住三个问题:
- 每种排序的核心动作是什么
- 它为什么会快或会慢
- 它适合什么场景,容易错在哪里
2. 学习目标¶
学完第 8 章后,建议至少达到下面这些目标:
- 能分清插入、交换、选择、归并、分配这几类排序思想。
- 能理解稳定性、时间复杂度、空间复杂度在排序里的真实含义。
- 能看懂并写出常见内部排序算法的真实
C代码。 - 能比较直接插入排序、冒泡排序、快速排序、堆排序、归并排序的优缺点。
- 能理解计数排序、基数排序为什么不完全依赖关键字比较。
- 能说清外部排序为什么要和磁盘 / 文件块联系起来理解。
3. 前置知识¶
继续往下看之前,最好已经比较稳地掌握:
本书第2章正文中的顺序表和数组下标操作。本书第5章正文中的完全二叉树概念。- 递归、分治、辅助数组这些常见代码组织方式。
这一章和前面章节的衔接关系很强:
- 堆排序和完全二叉树密切相关。
- 归并排序和分治思想密切相关。
- 快速排序和递归划分密切相关。
- 计数排序、基数排序和第 7 章中“用结构换效率”的思路也有呼应。
4. 本章结构总览¶
第 8 章可以按下面的顺序理解:
- 先抓排序的基本概念和评价指标。
- 再看插入类排序。
- 再看交换类排序。
- 再看选择类排序。
- 再看归并、计数、基数这些“更偏结构化处理”的排序。
- 最后再看各种内部排序的比较和外部排序。
5. 8.1 排序的基本概念¶
5.1 什么叫排序¶
排序就是:
- 把一组记录
- 按关键字
- 调整成递增或递减次序
真正要抓的是:
- 比较的是“关键字”
- 调整的是“记录位置”
5.2 稳定性为什么重要¶
稳定性指的是:
- 如果两个记录关键字相同
- 排序前谁在前面
- 排序后仍然谁在前面
这件事在单独一趟排序里看起来没那么显眼,但在:
- 多关键字排序
- 业务数据排序
- 基数排序的多趟分配
这些场景里会非常重要。
5.3 内部排序和外部排序¶
这一章前面大部分算法都属于内部排序:
- 数据基本都在内存里
- 重点优化比较和移动
而外部排序的重点则是:
- 数据量很大
- 需要频繁访问磁盘
- 要尽量减少 I/O 和归并趟数
6. 8.2 插入排序¶
6.1 直接插入排序的核心直觉¶
直接插入排序最适合的理解方式是:
- 把序列前面一段看成“已经排好”
- 每次拿一个新元素
- 往已排序区里插进去
它像在整理手里的扑克牌:
- 前面的牌已经排好了
- 新抓来一张
- 找到它应该插入的位置
6.2 折半插入排序¶
折半插入排序不是把“移动次数”减半,而是:
- 用折半查找更快地找到插入位置
- 但元素移动次数通常并不会因此消失
所以它优化的是:
- 定位插入位置的比较过程
而不是:
- 后移元素的成本
7. 8.3 交换排序¶
7.1 冒泡排序¶
冒泡排序最直观:
- 相邻元素比较
- 逆序就交换
- 每一趟把一个较大元素“冒”到后面
它代码不难,但很适合作为:
- 稳定性
- 提前结束优化
- 排序趟数
这些概念的练习入口。
7.2 快速排序¶
快速排序的核心动作是:
- 选一个枢轴
- 划分序列
- 让左边都不大于它,右边都不小于它
- 再递归处理左右两边
这是一种非常典型的分治算法。
8. 8.4 选择排序¶
8.1 简单选择排序¶
它每一趟做的事情很固定:
- 在未排序部分里找最小值
- 把它放到当前应在的位置
它的优点是交换次数少,思路直。
8.2 堆排序¶
堆排序最该抓的是:
- 它不是简单“挑最值”
- 而是先把序列组织成堆
- 再反复取堆顶最大值
所以堆排序的关键不是“排序过程”本身,而是:
建堆 + 调整堆
9. 8.5 归并排序、计数排序和基数排序¶
9.1 归并排序¶
归并排序的核心动作是:
- 先分
- 再治
- 最后把两个有序段归并成一个有序段
它的优势是思路稳定、复杂度稳定。
9.2 计数排序和基数排序¶
这两类排序特别值得和前面的“比较型排序”区分开。
它们更像是:
- 借助关键字范围或位数结构
- 减少直接比较带来的成本
所以它们的思路更偏“结构化分配与收集”。
10. 8.6 各种内部排序算法的比较及应用¶
这一部分后面会重点补:
- 时间复杂度比较
- 空间复杂度比较
- 稳定性比较
- 适用场景比较
11. 8.7 外部排序¶
这一部分后面会重点补:
- 为什么内存放不下就要考虑外部排序
- 为什么外部排序的核心不再只是比较次数
- 为什么多路归并和败者树这类结构会出现
12. 第8章代码阅读入口¶
第 8 章第一版代码目前已经先落了最值得优先掌握的内部排序算法:
- 直接插入排序
- 冒泡排序
- 快速排序
- 简单选择排序
- 堆排序
- 归并排序
- 计数排序
建议阅读顺序:
- 先看
InsertSort - 再看
BubbleSort - 再看
SelectionSort - 再看
QuickSort - 再看
HeapSort - 最后看
MergeSort和CountingSort
对应代码文件:
第8章普通代码版
16. 当前边界说明¶
这一版是“第 8 章启动版”,不是终版。
目前先把最适合直接代码落地、也最适合先学的内部排序部分起起来了。
还没有做厚的部分主要是:
- 各种排序算法的题型索引
- 原书试题区整理
- 外部排序的完整讲解
- 基数排序和外部排序的更完整代码专项
这些会在后续轮次继续补上。
16. 8.2 和 8.3 重点展开¶
16.1 直接插入排序为什么适合做“排序直觉起点”¶
直接插入排序很适合作为排序学习的第一站,因为它最像人在手工整理东西。
你可以把它想成整理扑克牌:
- 左手里前面几张牌已经排好了
- 新来一张牌
- 找到它该插进去的位置
- 把后面的牌整体往后挪一格
这个过程特别值得学生先看懂,因为后面很多排序题的理解都离不开这两件事:
- 怎么找到“该放的位置”
- 怎么把元素移动过去
16.2 直接插入排序的核心动作到底是什么¶
直接插入排序每一趟真正做的事,其实只有三步:
- 取出当前待插入元素
- 在已排序区中从后往前找位置
- 把比它大的元素整体后移
所以它的本质不是“交换”,而是:
插入前先挪空位。
这也是它和冒泡排序最容易混但其实最不一样的地方:
- 冒泡排序主要靠交换
- 直接插入排序主要靠后移 + 插入
16.3 直接插入排序:原书思路版伪代码¶
InsertSort(A, n)
for i = 1 to n - 1
temp = A[i]
j = i - 1
while j >= 0 and A[j] > temp
A[j + 1] = A[j]
j = j - 1
A[j + 1] = temp
学生看这段伪代码时,最该抓住的是:
temp为什么要先存下来- 为什么比较时是从后往前扫
- 为什么最后插回的是
j + 1
16.4 直接插入排序:对应真实代码¶
当前真实代码位置:
第8章普通代码版(约第31行)
对应函数:
InsertSort
这段代码和伪代码高度对应,很适合训练“从教材算法到真实代码”的转换能力。
尤其要注意:
temp保存当前待插入元素j从已排序区尾部开始回退- 回退过程中更大的元素整体后移
- 最后再把
temp放进空出来的位置
16.5 直接插入排序的复杂度和稳定性怎么理解¶
这类题不能只背结论,最好要有直觉。
时间复杂度:
- 最好情况:原序列本来就接近有序
- 最坏情况:序列完全逆序,每次都要大范围后移
所以它在“基本有序”场景下往往表现不错。
稳定性:
- 它是稳定的
- 因为相等元素通常不会被后来的相等元素跨过去
这条稳定性后面和选择排序、快速排序做对比时很重要。
16.6 折半插入排序到底优化了什么¶
这一点特别容易考判断题。
折半插入排序优化的是:
- 找插入位置时的比较过程
它没有本质优化的是:
- 元素后移次数
所以你要明确区分:
- 比较次数减少了
- 但移动次数不一定显著减少
这就是为什么折半插入排序听起来更高级,但在整体复杂度层面并不会神奇地变成对数级排序。
16.7 冒泡排序为什么看起来简单却仍值得学¶
很多学生会觉得冒泡排序“太基础,不值一学”,但它很适合用来练这些东西:
- 排序趟数
- 相邻交换
- 稳定性
- 提前结束优化
而且很多题就爱考它的这些基础点。
冒泡排序的画面感很强:
- 每比较一对相邻元素
- 如果逆序就交换
- 一趟结束后,一个较大元素会被推到后面
16.8 冒泡排序:原书思路版伪代码¶
BubbleSort(A, n)
for i = 0 to n - 2
flag = false
for j = 0 to n - 2 - i
if A[j] > A[j + 1]
swap(A[j], A[j + 1])
flag = true
if flag == false
break
这段伪代码最该抓的是:
- 为什么内层循环上界会越来越短
flag为什么能提前结束- 为什么说它是稳定排序
16.9 冒泡排序:对应真实代码¶
当前真实代码位置:
第8章普通代码版(约第51行)
对应函数:
BubbleSort
代码里最值得学生重点盯的是:
swapped标记- 每一趟结束后,末尾一段为什么可以不再参与比较
如果这两点看懂了,冒泡排序就不会停留在“会背名字”的层次。
16.10 冒泡排序的提前结束为什么重要¶
这条优化特别容易在选择题里出现。
如果某一趟扫描下来:
- 一次交换都没有发生
- 说明当前序列已经有序
- 后面就没必要继续做剩余趟数
所以 flag 的价值不是“代码更好看”,而是:
避免对已经有序的序列做多余工作。
16.11 快速排序最该抓的是“划分”¶
快速排序最容易学乱,因为学生经常把注意力全放在递归上。
但它真正的灵魂不在“递归”两个字,而在:
划分之后,枢轴左边都不大于它,右边都不小于它。
只要划分这一步看懂了,快速排序后面的递归其实就是自然展开。
16.12 快速排序:原书思路版伪代码¶
QuickSort(A, low, high)
if low < high
pivotpos = Partition(A, low, high)
QuickSort(A, low, pivotpos - 1)
QuickSort(A, pivotpos + 1, high)
其中关键是:
真正要学会的是:
- 枢轴不是“随便换来换去”
- 划分完成后,枢轴位置就确定了
- 后面递归时不再动这个位置
16.13 快速排序:对应真实代码¶
当前真实代码位置:
- 划分函数:
第8章普通代码版(约第95行)- 快速排序本体:
第8章普通代码版(约第118行)
对应函数:
PartitionQuickSort
学生看这段代码时最容易卡住的是:
low和high为什么都在动- 为什么不是每次都显式交换
- 为什么最后一定要把
pivot放回array[low]
所以这块后面非常适合做逐行拆解。
16.14 快速排序为什么平均快、但最坏会变差¶
这是快速排序最经典的理解点。
如果每次划分都比较均匀:
- 左右子区间规模差不多
- 递归树高度较低
- 整体效率会很好
但如果每次划分都特别不均匀,比如:
- 每次都把最小值或最大值当枢轴
- 导致一边几乎空,另一边几乎是全部元素
那快速排序就会退化得很厉害。
所以快速排序的关键,不只是“分”,而是:
分得是否均匀。
16.15 8.2 和 8.3 的高频易错点¶
这一部分当前最容易错的地方主要有:
- 把直接插入排序和冒泡排序混成一类“反正都在换位置”
- 折半插入排序时误以为移动次数也变成对数级
- 冒泡排序忘记
flag提前结束优化 - 快速排序把“递归”当重点,却没看懂
Partition - 快速排序划分后,错误地继续把枢轴所在位置也递归进去
- 快速排序最坏情况为什么会差,解释不清
17. 8.4 重点展开¶
17.1 简单选择排序最该抓的不是“交换”,而是“选最小”¶
简单选择排序和冒泡排序看起来都在换位置,所以学生很容易把它们混在一起。
但它们真正的核心动作完全不同:
- 冒泡排序是不断比较相邻元素,逆序就交换
- 简单选择排序是每一趟先在未排序区里找最小值,再一次性放到前面
所以选择排序更像:
- 先巡视一遍
- 确定谁最小
- 再把它换到当前该在的位置
17.2 简单选择排序每一趟到底在做什么¶
每一趟固定做三件事:
- 把当前起点位置当成“暂时最小值位置”
- 在后面的未排序区继续扫描
- 找到真正最小值后,再和当前起点交换
所以它和直接插入排序最大的区别是:
- 直接插入排序是在已排序区里给新元素找位置
- 简单选择排序是在未排序区里给当前位置找最小元素
17.3 简单选择排序:原书思路版伪代码¶
SelectionSort(A, n)
for i = 0 to n - 2
min = i
for j = i + 1 to n - 1
if A[j] < A[min]
min = j
if min != i
swap(A[i], A[min])
看这段伪代码时,最该抓的是:
min记录的是“当前最小元素的位置”- 内层循环不是在做交换,而是在找更小的值
- 真正交换通常一趟只发生一次
17.4 简单选择排序:对应真实代码¶
当前真实代码位置:
第8章普通代码版(约第75行)
对应函数:
SelectionSort
这段代码里最值得学生盯住的是:
min_index的更新过程- 为什么扫描完后才交换
- 为什么说它“交换次数少”,但比较次数并不一定少
17.5 简单选择排序的稳定性为什么容易错¶
这条很容易出判断题。
简单选择排序通常是不稳定的。
原因不是它一定大量交换,而是:
- 它在后面找到最小元素后
- 直接和前面当前位置交换
- 这个动作可能让相等关键字的相对次序发生变化
所以学生最容易掉坑的地方是:
- 看到它“一趟最多交换一次”
- 就误以为它稳定
但稳定性不看交换次数多少,而看:
相等元素的前后关系会不会被改变。
17.6 堆排序为什么看起来像“选择排序的升级版”¶
堆排序和简单选择排序之间有一个很值得学生抓住的联系:
- 它们都在不断把当前最值放到合适位置
区别在于:
- 简单选择排序每一趟都线性扫描去找最小值
- 堆排序先把序列组织成堆,让最大值直接出现在堆顶
所以堆排序可以理解成:
把“找最值”这件事结构化了。
这就是它比简单选择排序更强的地方。
17.7 堆排序最该抓的是“大根堆 + 下滤”¶
堆排序真正的核心,不是“交换堆顶和末尾”这个动作,而是:
- 如何建堆
- 如何在交换后恢复堆
更准确地说:
- 先把序列调整成大根堆
- 让堆顶永远是当前最大值
- 把堆顶换到当前末尾
- 再对剩余区间做一次下滤
所以堆排序的灵魂动作其实是:
下滤(SiftDown)
17.8 为什么堆排序和完全二叉树有关系¶
这一点是堆排序最容易“会写代码但没讲明白”的地方。
堆本质上是一棵完全二叉树,只不过通常用数组存。
所以:
- 某个下标位置的元素,对应树中一个结点
- 它的左右孩子位置可以直接通过下标算出来
这也是为什么堆排序代码里会出现类似:
2 * root + 12 * root + 2
这样的表达式。
学生如果把这层“数组下标 <-> 完全二叉树位置”看懂,堆排序代码会一下子顺很多。
17.9 堆排序:原书思路版伪代码¶
其中最关键的子过程是:
学生要特别注意:
- 每次交换堆顶和末尾后,末尾元素已经就位
- 真正还需要调整的,是前面剩余那部分堆
17.10 堆排序:对应真实代码¶
当前真实代码位置:
- 下滤:
第8章普通代码版(约第130行)- 堆排序本体:
第8章普通代码版(约第154行)
对应函数:
SiftDownHeapSort
看代码时最值得重点盯的是:
- 为什么总是先找左右孩子中更大的那个
- 为什么一旦父结点已经不小于较大孩子,就可以停
- 为什么每次把堆顶换到最后后,要缩小堆的有效范围
17.11 建堆为什么可以从最后一个非叶结点开始¶
这是堆排序里很经典、也很容易考的点。
原因是:
- 叶结点本身天然就是堆
- 不需要再调整
- 所以真正需要调整的,是那些有孩子的结点
- 从最后一个非叶结点开始向前做下滤,就能逐步把整棵树变成堆
这条结论如果理解了,学生就不会把建堆误解成“从根开始反复乱调”。
17.12 简单选择排序和堆排序怎么放在一起比较¶
这一组题非常适合对照记:
- 简单选择排序
- 每一趟线性扫描找最值
- 思路直观
- 不稳定
- 堆排序
- 先把找最值结构化成堆
- 每次更快地定位当前最大值
- 通常也不稳定
所以这两类排序虽然都属于“选择排序思想”,但层次完全不同:
- 一个是直接找
- 一个是借助堆来找
17.13 8.4 的高频易错点¶
这一部分当前最容易错的地方主要有:
- 把简单选择排序和冒泡排序混掉
- 误以为简单选择排序是稳定的
- 不理解堆排序里的数组下标为什么对应完全二叉树
- 只记得堆顶是最大值,却不会解释为什么
- 不会说明建堆为什么从最后一个非叶结点开始
- 交换堆顶和末尾后,忘记缩小堆的有效区间
18. 8.5 重点展开¶
18.1 归并排序最该抓的是“先分后并”¶
归并排序最容易被学生学成一句空话:分治法。
但真正要抓住的不是“分治”两个字,而是:
- 先把大问题拆成小问题
- 先让左右两边各自有序
- 再把两个有序段合成一个更大的有序段
所以归并排序最核心的动作不是“交换”,而是:
归并两个有序段。
这也是它和快速排序最容易混、但本质很不同的地方:
- 快速排序靠划分
- 归并排序靠合并
18.2 归并排序为什么稳定¶
归并排序通常是稳定的。
更准确地说,稳定性的关键不在“递归”,而在“归并时相等元素怎么处理”。
如果归并两个有序段时:
- 遇到相等元素
- 优先取左边那个
- 那就不会把原有相对次序打乱
这就是为什么代码里常会写成:
if (A[i] <= A[j])- 而不是只写成严格小于
这一点后面做稳定性比较题时很重要。
18.3 归并排序:原书思路版伪代码¶
MergeSort(A, left, right)
if left < right
mid = (left + right) / 2
MergeSort(A, left, mid)
MergeSort(A, mid + 1, right)
Merge(A, left, mid, right)
关键子过程:
学生看这段时最该抓的是:
- 递归不是目的,递归只是为了得到两个有序段
- 真正决定排序效果的,是
Merge
18.4 归并排序:对应真实代码¶
当前真实代码位置:
- 归并函数:
第8章普通代码版(约第171行)- 递归主体:
第8章普通代码版(约第200行)- 外壳函数:
第8章普通代码版(约第214行)
对应函数:
MergeMergeSortRecursiveMergeSort
学生看这段代码时,最值得重点盯的是:
i和j为什么分别指向两个有序段temp为什么必须存在- 为什么合并完后还要再写回原数组
18.5 归并排序为什么通常更稳,但空间开销更大¶
这一组判断题特别常见。
归并排序的优势在于:
- 思想稳定
- 时间复杂度比较稳定
- 稳定性也较好
但它的代价是:
- 通常要借助辅助数组
- 所以空间开销会更高
所以你不能只记“归并排序好”,而要记住:
- 它是用额外空间换来较稳定的排序过程
18.6 计数排序为什么和前面的排序思路不一样¶
计数排序特别值得学生从“思想切换”的角度看。
前面的插入、交换、选择、归并,核心都离不开关键字比较。
计数排序则不一样,它更像在做:
- 统计每个值出现了多少次
- 再按统计结果重建有序序列
所以它的核心不再是:
- 谁和谁比大小
而是:
- 这个值出现了几次
18.7 计数排序适合什么场景¶
计数排序最适合的不是“所有整数”,而是:
- 关键字范围不大
- 且通常是整数
- 范围已知或容易确定
因为它要开一个“计数数组”。
如果关键字范围特别大,但实际元素很少,那就会出现:
- 计数数组很大
- 空间利用很差
所以计数排序不能只看时间快不快,还要看值域是否合适。
18.8 计数排序:原书思路版伪代码¶
这段伪代码最该抓的是:
- 它不是通过不断比较来决定次序
- 而是通过“计数 -> 回填”来得到次序
18.9 计数排序:对应真实代码¶
当前真实代码位置:
第8章普通代码版(约第231行)
对应函数:
CountingSort
学生看这段代码时最值得重点盯的是:
- 为什么先把
counts清零 - 为什么要先扫描原数组做统计
- 为什么最后是按计数结果重新写回
18.10 基数排序最该抓的是“按位分配,再按位收集”¶
基数排序最容易学乱,因为学生经常只记一句“从低位到高位”。
更稳的理解方式是:
- 先按某一位把元素分到不同桶里
- 再按桶顺序收集回来
- 然后继续处理更高一位
所以基数排序的本质不是“整体直接排好”,而是:
一位一位地让序列越来越有序。
18.11 为什么基数排序通常要求稳定¶
这一点非常关键。
如果在处理某一位时,不稳定:
- 前面较低位已经建立起来的相对顺序
- 可能会被后续打乱
所以基数排序通常依赖:
- 每一趟按位排序必须稳定
这也是为什么教材里常把:
- 基数排序
- 稳定分配 / 收集
绑定在一起讲。
18.12 比较型排序和非比较型排序怎么区分¶
这一组题特别值得单独抓出来。
比较型排序包括:
- 插入排序
- 冒泡排序
- 快速排序
- 选择排序
- 堆排序
- 归并排序
它们的核心都离不开:
- 比较关键字大小
而计数排序、基数排序更偏:
- 利用关键字范围或位结构
- 通过统计、分配、收集得到次序
所以学生最该建立的是这个边界:
- 不同排序算法,不一定都在“不断比较”
18.13 8.5 的高频易错点¶
这一部分当前最容易错的地方主要有:
- 把归并排序和快速排序都简单归成“递归排序”,却说不清差别
- 归并排序代码里看不懂辅助数组
temp的作用 - 误以为计数排序适合所有整数排序问题
- 只会背基数排序“按位排”,但不理解为什么每一趟必须稳定
- 把计数排序、基数排序也当成普通比较型排序来理解
19. 8.6 重点展开¶
19.1 为什么这一节一定要做成“对照表思维”¶
很多学生学排序时最常见的问题,不是单个算法不会,而是:
- 每个算法单看好像都懂
- 一到比较题就开始混
- 稳定性、复杂度、适用场景全串了
所以 8.6 这一节最重要的任务,不是再讲一遍算法过程,而是:
把前面学过的排序算法放到同一张比较地图里。
只有这样,学生看到题目时才会从:
- “这是什么排序”
- 进化到
- “它为什么适合这个场景”
19.2 各种内部排序最该先比较哪四件事¶
比较内部排序时,最值得优先抓住四个维度:
- 时间复杂度
- 空间复杂度
- 稳定性
- 适用场景
这四个维度里,最容易被背成口号的是前两项。
真正更重要的是后两项:
- 稳定性决定相等关键字的相对次序是否保留
- 适用场景决定算法“什么时候值得用”
19.3 时间复杂度比较不能只背“最好、平均、最坏”¶
这一类题最容易学成表格背诵。
但更稳的理解方式是先问:
- 这个算法的主动作是什么
- 为什么这个动作在某些情况下会很顺
- 为什么某些情况下会变差
例如:
- 直接插入排序
- 基本有序时很友好
- 冒泡排序
- 提前结束时会更轻松
- 快速排序
- 划分均匀时很强
- 划分极不均匀时会恶化
- 堆排序
- 表现比较稳
- 归并排序
- 复杂度也很稳
所以复杂度不是死记结果,而是要和“为什么会这样”绑定。
19.4 空间复杂度比较最容易在哪掉坑¶
学生最容易在这两类算法上掉坑:
- 快速排序
- 归并排序
原因是:
- 快速排序看起来在原数组里排,好像很省空间
- 但递归调用本身也要占栈空间
- 归并排序则通常直接借助辅助数组
所以做空间复杂度题时,不要只盯“有没有另外开数组”,还要看:
- 递归栈
- 辅助数组
- 是否真正原地
19.5 稳定性最适合怎么记¶
稳定性如果只靠硬背,很容易把排序算法背混。
更好记的方法是看“相等关键字会不会被强行跨过”。
可以这样理解:
- 直接插入排序
- 通常稳定
- 因为相等元素不会被后来的相等元素越过去
- 冒泡排序
- 通常稳定
- 因为只在严格逆序时交换相邻元素
- 简单选择排序
- 通常不稳定
- 因为后面找到的最小值可能直接换到前面,打乱相等元素次序
- 快速排序
- 通常不稳定
- 因为划分过程中相等元素相对位置可能改变
- 堆排序
- 通常不稳定
- 归并排序
- 通常稳定,前提是归并时相等元素先取左边
所以稳定性最适合和“元素移动方式”一起记。
19.6 交换次数、移动次数、比较次数要分开看¶
这一点很容易在综合比较题里出坑。
很多学生一看到:
- 交换少
- 就觉得一定更快
但这并不总成立。
更稳的看法是:
- 比较次数
- 元素移动次数
- 交换次数
这三者不是一回事。
例如:
- 简单选择排序交换次数少
- 但比较次数并不低
所以做题时一定要看题目到底在比较哪一种代价。
19.7 哪些排序特别适合“基本有序”序列¶
这一类题很经典。
最值得先想到的是:
- 直接插入排序
因为它在“基本有序”情况下:
- 新元素通常不用挪太远
- 比较和移动都会减少
冒泡排序如果加了提前结束,也会比完全乱序时轻松。
所以这类题不要只会背算法名字,要能说清:
- 为什么它在基本有序时更占优势
19.8 哪些排序更适合数据量大且要求性能稳定¶
这一类题最容易让学生在快速排序和归并排序、堆排序之间犹豫。
更稳的思路是:
- 如果强调平均表现强,常会想到快速排序
- 如果强调最坏情况下也要稳定,归并排序、堆排序通常更值得关注
但还要继续细分:
- 归并排序更吃辅助空间
- 堆排序通常不稳定
所以场景题一定要连着看:
- 数据规模
- 稳定性要求
- 空间限制
19.9 为什么“哪种排序最好”通常是伪问题¶
这类题特别适合帮学生建立成熟判断。
排序里通常没有脱离场景的“绝对最好”。
更合理的问法应该是:
- 数据规模多大
- 是否基本有序
- 是否要求稳定
- 是否允许额外空间
- 是否是整数且值域有限
不同条件下,答案会完全不同。
所以学生到这一节最重要的成长之一,就是从:
- “背最优算法”
- 进化到
- “按场景选算法”
19.10 8.6 高频比较题统一抓法¶
这一节高频题通常可以归到下面几类:
- 复杂度比较题
- 稳定性判断题
- 空间复杂度判断题
- 场景选择题
- 算法分类题
- 插入类、交换类、选择类、归并类、分配类
做题时建议固定这样走:
- 先判断题目问的是“性质”还是“场景”
- 再锁定 2 到 3 个候选算法
- 最后用稳定性、空间、复杂度去排除
19.11 8.6 当前最容易丢分的点¶
这一节当前最容易丢分的地方主要有:
- 只会背复杂度表,不会解释原因
- 稳定性靠死记,容易把快速排序、堆排序、选择排序混掉
- 空间复杂度时漏看递归栈
- 把“交换次数少”和“整体代价低”混为一谈
- 场景题里不会同时看稳定性和空间限制
20. 8.7 重点展开¶
20.1 为什么外部排序和前面的排序问题已经不是一回事¶
前面的内部排序,默认都有一个很重要的前提:
- 数据基本都能放进内存
- 主要矛盾是比较次数和移动次数
但外部排序的前提变了:
- 数据量太大
- 内存放不下全部记录
- 需要频繁从磁盘读入、写回
这时真正的主要矛盾就不再只是:
- 比较次数多少
而变成:
- 读写磁盘多少次
- 要做多少趟归并
- 每一趟能归并多少路
所以外部排序和内部排序最大的区别,不是“算法名字不同”,而是:
优化目标已经从 CPU 内部操作,转向 I/O 代价。
20.2 外部排序最该抓的主线:先生成初始归并段,再多路归并¶
外部排序最核心的流程可以先记成两步:
- 先把大文件分批读入内存
- 每批在内存里排好,形成若干初始归并段
- 再对这些有序段反复做多路归并
所以外部排序真正的主体不是“原始数据直接一次排好”,而是:
- 先局部排好
- 再逐步归并成更大的有序段
如果学生把这条主线记住,后面的“归并趟数”“几路归并”“败者树”就都更容易挂上去。
20.3 为什么外部排序特别关心归并趟数¶
归并趟数之所以重要,是因为每多一趟归并,通常就意味着:
- 整批数据又要读一遍
- 再写一遍
也就是说,趟数每增加一次,I/O 成本都会明显增加。
所以外部排序的一个核心目标就是:
尽量减少归并趟数。
这也是为什么教材里会特别强调:
- 多路归并
- 初始归并段长度
- 败者树
这些结构和技巧。
20.4 为什么会有多路归并,而不是一直二路归并¶
如果每次都只做二路归并,那么:
- 归并层数会比较多
- 趟数也可能增加
于是外部排序更希望:
- 每次尽量同时归并更多路
- 让归并层数降下来
这就是多路归并出现的直接原因。
更形象地说:
- 二路归并像两条小河汇一条
- 多路归并像多条小河一次汇成大河
如果一次能合更多路,整体趟数通常更少。
20.5 多路归并为什么又不能无限增大路数¶
这也是一个特别值得解释清楚的点。
虽然路数越多,理论上归并趟数可能越少,但它不是越大越好。
因为路数增大后:
- 内存里要同时管理更多输入缓冲区
- 每次选最小记录也会更复杂
- 系统实现成本会上升
所以真正的设计思路不是“无限增大 k”,而是:
- 在内存容量、缓冲区数量和归并效率之间找平衡
20.6 败者树为什么会出现¶
败者树看起来像一个很“偏工程”的结构,但它的目标非常单纯:
让多路归并时反复选最小元素这件事更高效。
因为多路归并时,每次都要做一件事:
- 在若干路当前记录中找最小者
- 输出它
- 再从它所在那一路补进下一个记录
如果每次都从头比较一遍,代价会比较大。
于是败者树就相当于:
- 提前把比较关系组织起来
- 让下一次“选最小”变得更省事
学生这一节最该抓的不是实现细节,而是:
- 败者树是为“多路归并中的反复选最小”服务的
20.7 为什么外部排序也和归并排序有天然联系¶
这一点很适合拿来帮助学生建立前后章节联系。
内部排序里的归并排序是:
- 先分
- 再把两个有序段归并
外部排序的主线则是:
- 先形成若干个已经有序的归并段
- 再把这些归并段继续归并
所以外部排序其实可以看成:
- 归并思想在“大数据 + 外存”环境下的延伸
区别在于:
- 内部排序更关心比较和递归
- 外部排序更关心 I/O、缓冲区和归并趟数
20.8 8.7 高频题最常考什么¶
这一节高频题通常集中在下面几类:
- 外部排序和内部排序的区别
- 初始归并段的含义
- 多路归并为什么能减少趟数
- 归并趟数计算题
- 败者树的作用
- 缓冲区和路数的关系
所以做这类题时,先问自己:
- 它在考“概念”
- 还是在考“趟数计算”
- 还是在考“结构作用”
20.9 8.7 当前最容易丢分的点¶
这一节当前最容易错的地方主要有:
- 还在用内部排序的思维看外部排序
- 只会背“多路归并减少趟数”,但说不清为什么
- 不理解败者树是为谁服务的
- 归并趟数题时不会把“初始归并段个数”和“每趟归并路数”对应起来
- 以为路数越大一定越好
21. 第8章题型索引¶
21.1 基本概念与性质类¶
这一类题主要围绕:
- 排序的定义
- 稳定性
- 内部排序与外部排序
- 比较型排序与非比较型排序
这类题表面像概念题,实际上在考:
- 你是否理解排序评价维度
- 你是否会把某个排序算法放进正确类别
21.2 插入排序与交换排序类¶
这一类题主要包括:
- 直接插入排序过程题
- 折半插入排序优化点
- 冒泡排序过程题
- 冒泡排序提前结束
- 快速排序划分过程题
- 快速排序最坏情况分析
这类题最核心的是:
- 看懂每一趟做了什么
- 分清“插入”“交换”“划分”三种动作
21.3 选择排序与堆排序类¶
这一类题主要包括:
- 简单选择排序每趟找最小值
- 简单选择排序稳定性判断
- 大根堆 / 小根堆性质
- 建堆过程
- 堆排序下滤过程
- 堆排序与完全二叉树对应关系
这类题最该抓的是:
- 选择排序在“找最值”
- 堆排序在“结构化地找最值”
21.4 归并、计数、基数排序类¶
这一类题主要包括:
- 归并排序递归过程
- 归并操作过程
- 归并排序稳定性和空间复杂度
- 计数排序使用条件
- 基数排序按位分配与收集
- 为什么基数排序要求稳定
这类题最该抓的是:
- 归并排序靠“合并有序段”
- 计数、基数排序不完全依赖关键字比较
21.5 综合比较类¶
这一类题主要包括:
- 时间复杂度比较
- 空间复杂度比较
- 稳定性比较
- 场景选择题
- “哪种排序更适合基本有序序列”之类的综合判断
这类题最该抓的是:
- 不能脱离场景谈“最好”
- 要同时看稳定性、空间和数据特点
21.6 外部排序类¶
这一类题主要包括:
- 外部排序与内部排序的区别
- 初始归并段
- 多路归并
- 归并趟数
- 败者树作用
这类题最该抓的是:
- 目标已经从比较次数转成 I/O 代价
- 趟数和路数之间的关系
22. 刷题路径¶
22.1 基础训练:先把插入、冒泡、选择三类基础排序做稳¶
基础训练建议先刷:
- 直接插入排序
- 折半插入排序
- 冒泡排序
- 简单选择排序
目标是:
- 先把“每一趟到底在做什么”真正弄清楚
- 把稳定性和基本复杂度先建立起来
22.2 进阶训练:专刷快速排序和堆排序¶
进阶训练建议集中刷:
- 快速排序划分题
- 快速排序最坏情况题
- 建堆题
- 下滤题
- 堆排序过程题
这一部分最重要的是:
- 把
Partition看懂 - 把“数组下标 <-> 完全二叉树”看懂
22.3 强化训练:专刷归并、计数、基数排序¶
强化训练建议集中刷:
- 归并过程题
- 归并排序稳定性和空间复杂度
- 计数排序适用条件题
- 基数排序按位处理题
- 比较型 / 非比较型排序区别题
这一部分的目标是:
- 把“合并”和“分配 / 收集”这两种思维区分开
22.4 第四轮:专刷综合比较题¶
第四轮建议集中刷:
- 时间复杂度比较
- 空间复杂度比较
- 稳定性比较
- 场景选算法题
这一部分最重要的是:
- 不再死记表格
- 而是能根据场景做判断
22.5 第五轮:专刷外部排序¶
第五轮建议集中刷:
- 初始归并段
- 多路归并
- 归并趟数
- 败者树
这一部分的目标是:
- 把外部排序和内部排序彻底区分开
- 理解为什么排序目标已经变成减少 I/O
22.6 第六轮:整章综合回刷¶
最后一轮建议整章混着刷:
- 先判断题目属于哪类
- 再调动对应算法和比较维度
这一部分的目标不是“第一次学会”,而是:
- 看见题就能迅速分类
- 知道该从哪条主线切进去
23. 自学检查清单¶
23.1 概念层¶
学完第 8 章后,至少应该能不用看书直接说清:
- 稳定性是什么意思
- 内部排序和外部排序的区别
- 比较型排序和非比较型排序的区别
- 为什么排序算法不能脱离场景比较“绝对最好”
23.2 算法过程层¶
你至少应该能做到:
- 手推直接插入排序每一趟
- 手推冒泡排序每一趟
- 手推快速排序一次划分
- 手推简单选择排序每一趟
- 手推建堆和一次下滤
- 手推归并两个有序段
23.3 性质比较层¶
你至少应该能做到:
- 分清哪些排序稳定、哪些不稳定
- 分清哪些排序更适合基本有序序列
- 分清哪些排序更依赖额外空间
- 说清快速排序、堆排序、归并排序各自的优缺点
23.4 非比较型排序层¶
你至少应该能做到:
- 说清计数排序适合什么条件
- 说清基数排序为什么按位处理
- 说清基数排序为什么通常要求稳定
- 分清计数排序、基数排序和比较型排序的根本区别
23.5 外部排序层¶
你至少应该能做到:
- 说清初始归并段是什么
- 说清为什么要多路归并
- 说清归并趟数为什么重要
- 说清败者树是为谁服务的
23.6 代码层¶
你至少应该能做到:
- 看懂
第8章普通代码版中的插入、冒泡、选择、快速排序 - 看懂
第8章普通代码版中的SiftDown和HeapSort - 看懂
第8章普通代码版中的Merge和MergeSort - 看懂
第8章普通代码版中的CountingSort - 卡住时能切到
第8章逐行注释版代码
23.7 阶段总结¶
到这里,第 8 章已经从“启动版”进入了“章节成品化中段”:
- 主线讲义已经搭起来
- 代码首稿已经完成
- 逐行注释版已经补上
- 题型索引、刷题路径、自学检查也已经补上
这意味着后面继续推进原书试题整理和详细解答时,已经有很稳的底座可用。
24. 原书试题区整理版¶
24.1 这一版整理的作用¶
这一部分不是把原书题目原样搬过来,而是把原书第 8 章试题按“考法”重新归类。
这样整理的价值主要有两点:
- 学生做题前,能先判断题目属于哪一类,不会把整章排序题看成一团
- 后面继续补逐题详细解答时,可以直接沿着这个分类往下展开
这一版目前先做三件事:
- 把原书试题按知识块重新分组
- 给出每类题在考什么
- 给出对应的做题思路和易错点
24.2 基本概念与性质题¶
这一类题通常围绕:
- 稳定性
- 内部排序与外部排序
- 比较型排序与非比较型排序
- 时间复杂度、空间复杂度的基本判断
这类题表面上像“记忆题”,其实真正考的是:
- 你是否理解每种排序的本质动作
- 你能不能把某种性质和对应算法对上
做题思路:
- 先判断题目问的是稳定性、复杂度还是分类
- 再回到算法动作本身去解释
- 不要只靠背表格
易错点:
- 只记名字,不记动作
- 把稳定性和“交换次数多少”混为一谈
- 把外部排序也按内部排序思路来理解
24.3 插入排序与交换排序题¶
这一类题通常围绕:
- 直接插入排序每一趟的过程
- 折半插入排序优化了什么
- 冒泡排序每一趟的结果
- 冒泡排序提前结束条件
- 快速排序划分过程
- 快速排序最坏情况分析
这类题最常考的不是大定义,而是:
- 每一趟到底做了什么
- 某一步之后序列变成什么样
- 为什么快排会好,也为什么会退化
做题思路:
- 直接插入排序:盯“取出、后移、插回”
- 冒泡排序:盯“相邻比较、逆序交换”
- 快速排序:盯“枢轴划分”而不是只盯递归
易错点:
- 把直接插入排序和冒泡排序都看成“换位置”
- 把折半插入排序误解成整体复杂度也变成对数级
- 快速排序没看懂
Partition就开始强行背结论
24.4 选择排序与堆排序题¶
这一类题通常围绕:
- 简单选择排序每一趟选最小值
- 简单选择排序稳定性判断
- 大根堆 / 小根堆性质
- 建堆过程
- 下滤过程
- 堆排序每一趟结果
这类题最核心的考点有两条:
- 选择排序是在“扫描未排序区找最值”
- 堆排序是在“用堆结构化地找最值”
做题思路:
- 选择排序题:每一趟先找最小位置,再看交换结果
- 堆排序题:先判断当前是不是合法堆,再看是否需要下滤
- 建堆题:从最后一个非叶结点开始向前调
易错点:
- 误以为简单选择排序稳定
- 不会把数组下标和完全二叉树位置对应起来
- 堆排序时忘记堆的有效区间会逐步缩小
24.5 归并、计数、基数排序题¶
这一类题通常围绕:
- 归并两个有序段
- 归并排序递归过程
- 归并排序稳定性和空间复杂度
- 计数排序适用条件
- 基数排序按位分配 / 收集
- 为什么基数排序要求稳定
这类题的真正核心是:
- 归并排序在做“合并有序段”
- 计数排序和基数排序不再主要依赖关键字比较
做题思路:
- 归并题:盯两个有序段的双指针合并
- 计数排序题:先看值域是否合适
- 基数排序题:先看按哪一位分配,再看收集顺序
易错点:
- 把归并排序和快速排序都只记成“递归排序”
- 以为计数排序适合所有整数排序
- 记住了基数排序“按位排”,却不理解稳定性的作用
24.6 综合比较题¶
这一类题通常围绕:
- 时间复杂度比较
- 空间复杂度比较
- 稳定性比较
- 哪种算法更适合某种场景
- 哪种算法适合基本有序、数据量大、稳定性要求高等条件
这类题最容易拉开差距,因为它在考:
- 你是不是只会背单个算法
- 还是已经能把整章放到一张比较图里
做题思路:
- 先判断题目问的是“性质比较”还是“场景选算法”
- 再锁定候选算法
- 最后用稳定性、空间、复杂度和数据特点去排除
易错点:
- 试图脱离场景谈“哪个排序最好”
- 只会背复杂度,不会结合稳定性和空间限制
24.7 外部排序题¶
这一类题通常围绕:
- 外部排序与内部排序的区别
- 初始归并段
- 多路归并
- 归并趟数
- 败者树作用
这类题最该抓的是:
- 排序目标已经从比较次数转成 I/O 代价
- 趟数、路数、缓冲区这些量之间有关系
做题思路:
- 区分题目是在考概念还是在考归并趟数
- 看清有多少初始归并段
- 看清每趟是几路归并
易错点:
- 不会把“初始归并段个数”和“归并路数”联系起来
- 只会背“败者树更高效”,却说不清它在优化什么
24.8 这一版整理后的使用方法¶
现在最推荐这样用这部分:
- 做题前先看题型分类,判断题目属于哪一类
- 做题时按该类题的固定思路去推
- 做完后再回看对应章节内容和代码
这样用会比“从头往后乱刷”更稳,因为它会强迫学生先建立题型识别能力。
25. 原书试题逐题详细解答版(基础部分)¶
25.1 高频题 1:为什么直接插入排序通常适合基本有序序列¶
题目本质:
这类题不是只问“哪个算法快”,而是在问:
当序列已经接近有序时,哪个算法的额外工作会明显减少。
正确结论:
- 直接插入排序通常很适合基本有序序列
详细思路:
- 直接插入排序每次把一个新元素插入前面已排序区
- 如果序列本来就基本有序,那么大多数元素已经离正确位置不远
- 这样从后往前找插入位置时,比较次数和后移次数都会减少
- 所以它在这种场景下会表现得比完全乱序时更轻松
易错点:
- 只会背“插入排序适合基本有序”,却说不出原因
- 把“适合基本有序”和“整体复杂度已经变成最优”混为一谈
25.2 高频题 2:折半插入排序到底优化了什么¶
题目本质:
这类题在考你是不是分清了“比较”和“移动”这两类代价。
正确结论:
- 折半插入排序主要优化的是查找插入位置时的比较过程
- 它并没有从根本上消除元素移动
详细思路:
- 直接插入排序中,有两部分工作:
- 找插入位置
- 后移元素腾位置
- 折半插入排序只是把“找位置”改成了更快的折半定位
- 但一旦位置确定,该后移的元素还是要后移
- 所以不能简单把它理解成“整体代价突然变得特别低”
易错点:
- 以为折半插入排序会让整个排序复杂度都变成对数级
- 不会区分比较次数减少和移动次数不变
25.3 高频题 3:冒泡排序为什么加了标志位就能提前结束¶
题目本质:
这类题在考的是“排序过程中如何发现序列已经有序”。
正确结论:
- 如果某一趟扫描下来一次交换都没有发生,说明序列已经有序
- 后面的趟数可以直接省掉
详细思路:
- 冒泡排序每次只在相邻逆序时交换
- 如果一整趟都没有发生交换,说明所有相邻元素都已经满足局部有序
- 相邻元素都已经有序时,整个序列也就已经有序
- 所以后续不需要再继续做无意义的扫描
易错点:
- 只记得有个
flag,但不知道它到底在判断什么 - 误以为提前结束只是“代码优化小技巧”,看不到它背后的排序状态判断
25.4 高频题 4:快速排序为什么最该盯 Partition¶
题目本质:
这类题在考你是不是抓住了快速排序的灵魂动作。
正确结论:
- 快速排序最核心的是划分
- 枢轴一旦划分完成,其最终位置就确定了
详细思路:
- 快速排序不是先递归才有意义,而是先通过划分把一个元素放到正确位置
- 划分后,左边元素都不大于枢轴,右边元素都不小于枢轴
- 这样左右两边就变成两个规模更小但同类的问题
- 递归只是把这个动作在左右子区间继续重复
易错点:
- 只盯“递归”两个字,没看懂划分
- 以为快速排序每次都能平均分开,忽略最坏情况
25.5 高频题 5:为什么简单选择排序通常不稳定¶
题目本质:
这类题在考的是“稳定性到底由什么决定”。
正确结论:
- 简单选择排序通常不稳定
详细思路:
- 每一趟选择排序会在后面未排序区里找到最小元素
- 然后把它直接交换到前面
- 这个交换动作可能让两个关键字相同的元素前后顺序发生变化
- 所以它不是稳定排序
更重要的是要形成一个判断习惯:
- 稳定性不看“交换次数少不少”
- 而看“相等元素的相对顺序会不会变”
易错点:
- 认为“一趟只交换一次,所以稳定”
- 把稳定性和交换次数多少混掉
25.6 高频题 6:堆排序建堆为什么从最后一个非叶结点开始¶
题目本质:
这类题在考的是你是否理解“哪些结点需要调整,哪些不需要”。
正确结论:
- 建堆时从最后一个非叶结点开始向前调整
详细思路:
- 叶结点本身没有孩子,所以天然满足堆的局部性质
- 真正可能不满足堆性质的,是那些有孩子的结点
- 最后一个非叶结点后面的全是叶子
- 所以从最后一个非叶结点开始,向前逐个做下滤调整,就能逐步把整个序列整理成堆
易错点:
- 以为建堆必须从根开始
- 只记住公式,不知道为什么叶子结点不用调
25.7 高频题 7:归并排序为什么通常稳定¶
题目本质:
这类题在考的是“稳定性和归并过程之间的关系”。
正确结论:
- 归并排序通常是稳定的
- 前提是归并时相等元素优先取左边
详细思路:
- 归并排序真正的关键是把两个有序段合并
- 如果两个有序段当前元素相等,而代码选择先放左段元素
- 那么原来左边出现的元素仍然会排在右边那个相等元素前面
- 这样相等元素的相对次序就保住了
易错点:
- 只会背“归并排序稳定”,却说不清为什么
- 不知道稳定性和归并时相等元素的处理规则有关
25.8 高频题 8:计数排序为什么不适合值域特别大的情况¶
题目本质:
这类题在考的是“算法适用条件”。
正确结论:
- 计数排序更适合关键字范围不大的整数序列
- 值域过大时,空间代价可能非常不划算
详细思路:
- 计数排序要开一个计数数组
- 这个数组大小和关键字值域直接相关
- 如果值域很大,但实际元素数量并不多
- 就会出现“数组很大,但很多位置都空着”的情况
- 这样空间利用率会很差
易错点:
- 只看到计数排序很快,就觉得所有整数排序都该优先用它
- 忽略了值域大小这个前提
25.9 高频题 9:为什么基数排序通常要求每一趟稳定¶
题目本质:
这类题在考的是“多趟排序之间的前后衔接关系”。
正确结论:
- 基数排序通常要求每一趟按位排序稳定
详细思路:
- 基数排序不是一趟完成,而是按位一趟一趟处理
- 低位先建立起来的相对次序,后面高位排序时不能被随便打乱
- 如果某一趟不稳定,前面已经形成的有序关系就可能被破坏
- 那样最终结果就不可靠
易错点:
- 只背“基数排序稳定”,却说不清为什么
- 不能把“低位已经排出的相对关系”这件事说清楚
25.10 高频题 10:为什么说“哪种排序最好”通常是伪问题¶
题目本质:
这类题在考的是你是否已经从“背算法”进入“按场景选算法”。
正确结论:
- 排序算法很少有脱离场景的绝对最好
详细思路:
- 有的题强调基本有序
- 有的题强调稳定性
- 有的题强调内存限制
- 有的题强调值域大小
- 有的题强调最坏情况也要可控
不同条件下,最合适的排序算法会完全不同。
所以真正成熟的回答方式应该是:
- 先看数据特征
- 再看稳定性要求
- 再看空间限制
- 最后再选算法
易错点:
- 总想背一个“万能最优排序”
- 场景题里不同时考虑稳定性和空间条件
25.11 高频题 11:外部排序为什么特别在意归并趟数¶
题目本质:
这类题在考的是“外部排序优化目标已经变了”。
正确结论:
- 外部排序特别关心归并趟数,因为每多一趟通常都意味着整批数据又要多读写一次
详细思路:
- 外部排序的数据通常在磁盘上
- 每一趟归并,往往都要把数据重新读入、再写出
- 所以趟数越多,I/O 成本就越大
- 这也是为什么要强调多路归并和败者树这类结构
易错点:
- 还在用内部排序的比较次数思维看外部排序
- 只会背“趟数越少越好”,但说不清为什么
25.12 基础题解学习提示¶
这一部分先覆盖的是第 8 章最核心、最能建立题感的一批题:
- 插入排序适用场景
- 折半插入排序优化边界
- 冒泡排序提前结束
- 快速排序的划分主线
- 选择排序稳定性
- 堆排序建堆
- 归并排序稳定性
- 计数排序适用条件
- 基数排序稳定性
- 外部排序归并趟数
这意味着第 8 章现在已经不只是有“题型目录”,而是开始有“逐题详解层”了。
25.13 高频题 12:快速排序过程题为什么一定要先把枢轴路径走完整¶
题目本质:
这类题在考的不是你会不会背代码,而是你能不能把一次划分过程真正手推出来。
正确结论:
- 快速排序过程题最稳的做法,是先固定枢轴,再完整走完一次划分路径
详细思路:
- 先确定当前枢轴是谁
- 再看左右指针或左右扫描是怎么移动的
- 每移动一步,都要明确当前在找什么:
- 右边找比枢轴小的
- 左边找比枢轴大的
- 最后枢轴落到的位置,就是它的最终位置
- 这个位置后面不再参与本层递归
所以过程题里最怕的不是不会递归,而是:
- 枢轴还没放稳
- 就急着看左右子区间
易错点:
- 枢轴还没归位就提前递归
- 划分完后又把枢轴位置继续算进同一层子问题
- 只盯序列变化,不盯“枢轴位置已经确定”这件事
25.14 高频题 13:堆排序每一趟结果题怎么做最稳¶
题目本质:
这类题在考你是否把“堆顶取最大值”和“剩余部分重新成堆”这两步分清楚了。
正确结论:
- 堆排序每一趟都先把堆顶换到末尾
- 然后只对前面剩余部分重新做下滤
详细思路:
- 先确认当前序列是不是大根堆
- 把堆顶最大值和当前末尾交换
- 交换后,末尾元素已经就位,不再参与后续建堆
- 再对前面剩余区间从根开始做一次下滤
- 这样才得到“下一趟开始前”的合法堆
所以这类题固定节奏是:
- 看交换
- 看有效区间缩小
- 看下滤恢复
易错点:
- 交换完堆顶和末尾后,忘记下滤
- 继续把已经排好位置的末尾元素算进堆里
- 只会看最终结果,不会写中间每一趟结果
25.15 高频题 14:归并两个有序段时为什么一定要双指针同步看¶
题目本质:
这类题在考的是归并操作本身,而不是整个递归框架。
正确结论:
- 归并两个有序段时,要用两个指针分别指向两个有序段当前未处理的最前元素
详细思路:
- 两个子段本身都已经有序
- 所以每次只要比较两个子段当前最前面的元素
- 较小者先放入结果数组
- 对应指针后移
- 一段耗尽后,把另一段剩余元素整体接上
这类题真正关键的不是“记住代码”,而是:
- 明白为什么只比较当前两个最前元素就够了
易错点:
- 归并时还想回头看已经处理过的元素
- 一段结束后,忘记把另一段剩余部分整体接上
- 把归并排序和快速排序的过程混掉
25.16 高频题 15:为什么归并排序通常时间复杂度更稳定¶
题目本质:
这类题在考你是否理解“归并排序的工作量为什么比较规律”。
正确结论:
- 归并排序的时间复杂度通常比较稳定,因为它总是按比较固定的方式分解和合并问题
详细思路:
- 不管原序列初始分布怎样
- 归并排序都会持续把问题一分为二
- 再把两个有序段合并回来
- 这种“分 + 合”的结构比较稳定
- 所以它不像快速排序那样特别依赖划分是否均匀
易错点:
- 只会背结论,不会和快速排序对比着理解
- 把“递归排序”都想成一样稳定
25.17 高频题 16:比较题里为什么不能把快速排序、堆排序、归并排序只按复杂度排队¶
题目本质:
这类题在考你是不是已经从“背表格”过渡到“看场景选算法”。
正确结论:
- 快速排序、堆排序、归并排序不能只看时间复杂度,还要看稳定性、空间代价和最坏情况表现
详细思路:
- 快速排序平均表现通常很好,但最坏情况会恶化
- 堆排序性能较稳,但通常不稳定
- 归并排序也较稳,而且通常稳定,但要额外辅助空间
- 所以这三者没有脱离场景的绝对胜负
真正成熟的判断方式是:
- 先看数据规模
- 再看稳定性要求
- 再看空间限制
- 再看是否要控制最坏情况
易错点:
- 只看到平均复杂度就断言谁最好
- 忽略稳定性和额外空间
- 不会把“最坏情况是否可控”纳入判断
25.18 高频题 17:为什么计数排序和基数排序经常放在一起考¶
题目本质:
这类题在考你是否真正建立了“非比较型排序”这个类别。
正确结论:
- 计数排序和基数排序经常放在一起考,因为它们都不主要依赖关键字两两比较来确定次序
详细思路:
- 计数排序通过统计出现次数来恢复有序序列
- 基数排序通过按位分配和收集逐步建立有序性
- 它们和插入、交换、选择、归并这类排序的思维方式明显不同
- 所以教材和试题中常把它们作为“非比较型排序”来对照考察
易错点:
- 只记住两个算法名字,不知道它们为什么经常一起出现
- 不能说清它们和比较型排序的边界
25.19 高频题 18:外部排序为什么要同时考虑初始归并段和归并路数¶
题目本质:
这类题在考你是否真正理解归并趟数是怎么来的。
正确结论:
- 外部排序的归并趟数,同时取决于初始归并段个数和每一趟归并的路数
详细思路:
- 初始归并段越多,后面需要归并的对象越多
- 每一趟归并路数越大,一次能合并掉的段就越多
- 所以两者一起决定“还要做几轮归并”
- 这也是为什么外部排序题里常常同时给出:
- 初始归并段个数
- 可做几路归并
易错点:
- 只看路数,不看初始归并段个数
- 只背“多路归并趟数少”,却不会把它用到具体题里
25.20 进阶题解学习提示¶
这一部分覆盖的是第 8 章里最容易拉开差距的过程题和综合比较题:
- 快速排序划分过程
- 堆排序每一趟结果
- 归并过程
- 归并排序与快速排序的稳定性 / 复杂度对照
- 非比较型排序边界
- 外部排序的归并趟数理解
学完这一部分后,第 8 章已经不只是“有题型目录”,而是开始具备比较完整的题解层了。
25.21 排序代码专题:基数排序与外部排序¶
这里把第 8 章之前最明显的两个代码缺口补齐了:
- 基数排序真实代码
- 外部排序流程模拟代码
这两块很关键,因为它们正好对应了“前面内部排序主线”和“后面外部排序主线”里最容易让学生回原 PDF 的地方。
25.21.1 基数排序的真实代码覆盖内容¶
代码中已经补入以下内容:
第8章普通代码版里的GetMaxValue第8章普通代码版里的CountingSortByDigit第8章普通代码版里的RadixSortLSD
这三段代码连起来,正好把基数排序最该抓的主线落成了真实程序:
- 先确定最高位需要处理到哪里
- 再从个位开始
- 每一趟都按当前位做一次稳定计数
- 一位一位把整体有序性“搭起来”
这样学生以后再看到“为什么基数排序要求稳定”这类题,就不需要只背定义了。
直接对着 CountingSortByDigit 看就能明白:
- 这一趟只按当前位分类
- 如果同位元素的原相对顺序乱了
- 前面已经建立好的低位有序性就会被破坏
当前程序运行时已经能直接看到:
Radix sort: 329 355 436 457 657 720 839
这意味着基数排序现在不只是“讲义里会说”,而是已经能真正跑出来。
25.21.2 外部排序的真实代码覆盖内容¶
外部排序这一部分不再停留在概念说明,而是配套给出一个“外部归并流程模拟版”的真实程序:
第8章普通代码版里的PrintRuns第8章普通代码版里的MergePass第8章普通代码版里的ExternalMergeSortSimulation
这三段代码最重要的作用,不是模拟磁盘 I/O 的所有细节,而是把外部排序最核心的流程跑出来:
- 先生成初始归并段
- 再按段长一趟一趟归并
- 每归并一轮,归并段会变大
- 最后全部并成一个有序结果
这正好对应了教材里最常考的那条主线:
- 初始归并段是什么
- 归并路数和归并趟数怎么影响总流程
- 为什么外部排序最在意“段”和“趟”
当前程序里已经能直接看到这些关键输出:
Initial runsAfter merge pass 1After merge pass 2External merge sort simulation
也就是说,现在学生已经可以不靠原书图示,也能直接从程序输出看懂:
- 初始归并段长什么样
- 每一趟归并后段是怎么变大的
- 为什么归并趟数会影响整体代价
25.21.3 代码层能力提升¶
学完这一部分以后,第 8 章代码层终于形成了更完整的闭环:
- 比较型排序有真实代码
- 非比较型排序不再只剩计数排序
- 外部排序不再只有概念,没有程序支撑
这一步很关键,因为它直接减少了第 8 章中回原 PDF 查阅以下内容的需要:
- 基数排序按位处理过程
- 外部排序初始归并段
- 多趟归并流程图
25.22 外部排序专题:败者树与置换-选择¶
在已经补足“外部排序主流程”的基础上,这里继续补入外部排序中最容易在题目里出现的两块内容:
- 败者树
- 置换-选择
25.22.1 败者树的真实代码覆盖内容¶
代码中已经补入以下内容:
第8章普通代码版里的LoserTreeAdjust第8章普通代码版里的BuildLoserTree第8章普通代码版里的LoserTreeDemo
这三段代码最重要的意义,不是把败者树写成一个特别庞杂的工程组件,而是把它最该让学生理解的那条主线跑出来:
- 多路归并时,并不是每次都把所有路重新完整比较一遍
- 败者树把“上一轮输掉的比较关系”保留下来
- 所以下一轮只需要沿一条路径调整
这正是教材和试题里那句“败者树能减少关键字比较”的真正落地点。
现在学生不需要只背这句话了,因为程序已经能直接输出:
Loser tree merge order: 5 7 9 12 18
也就是说,败者树现在已经不只是一个抽象名词,而是有了可观察的“选优过程”。
25.22.2 置换-选择的真实代码覆盖内容¶
置换-选择这块,代码中已经补入以下内容:
第8章普通代码版里的ReplacementSelectionRuns
这段代码虽然是教学版实现,但已经把这类题最该掌握的核心机制跑出来了:
- 内存区先装一批记录
- 每次输出当前可输出的最小记录
- 新读入的记录如果还能接在当前归并段后面,就继续留在本段
- 如果已经接不上,就冻结到下一段
这一步非常重要,因为学生以前最容易只记结果:
- 置换-选择能生成更长的初始归并段
但说不清“为什么更长”。
现在对着程序输出,就能更形象地理解:
- 当前段并不是机械地固定长度
- 它会尽量把还能继续保持非降序的记录都接进去
- 真接不上的记录,才被推迟到下一段
当前程序已经能直接看到:
Replacement selection runsRun 1Run 2
这就把“为什么初始归并段会变长”从一句结论,变成了一个可观察的过程。
25.22.3 外部排序能力提升¶
学完这一部分以后,第 8 章外部排序已经形成了比以前完整得多的支撑链:
- 有概念解释
- 有流程模拟
- 有归并段输出
- 有败者树选优演示
- 有置换-选择生成初始归并段的真实程序
这意味着学生现在再遇到:
- “为什么要用败者树”
- “置换-选择为什么能增加初始归并段长度”
- “外部排序为什么不只是简单归并”
这几类题时,已经更不需要回原 PDF 查原图和原过程了。
站内代码入口¶
- 对应代码专题:第8章 排序代码
- 如果你想逐行对照实现,建议把正文页和代码专题页一起打开。