这一篇不再详细讲过程,重点解决408选择题:
复杂度、稳定性、空间、初始状态影响、适用场景、外部排序。
一、先看408最重要总表
| 排序算法 | 最好 | 平均 | 最坏 | 空间 | 稳定性 |
|---|---|---|---|---|---|
| 直接插入 | O(n) | O(n²) | O(n²) | O(1) | 稳定 |
| 折半插入 | O(n²)* | O(n²) | O(n²) | O(1) | 稳定 |
| 希尔 | 与增量有关 | 与增量有关 | 可到O(n²) | O(1) | 不稳定 |
| 冒泡 | O(n) | O(n²) | O(n²) | O(1) | 稳定 |
| 快速 | O(n log n) | O(n log n) | O(n²) | O(log n)平均 | 不稳定 |
| 简单选择 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 |
| 堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | 不稳定 |
| 二路归并 | O(n log n) | O(n log n) | O(n log n) | O(n) | 稳定 |
| 基数 | O(d(n+r)) | 同左 | 同左 | O(n+r) | 稳定 |
说明:
折半插入虽然比较次数可降到O(n log n),
但元素移动仍可能O(n²),所以整体仍是O(n²)。
二、稳定性秒杀表
稳定
直接插入
折半插入
冒泡
归并
基数
不稳定
希尔
快速
简单选择
堆
可以记成:
稳定:插冒归基
不稳定:希快选堆
其中"插"包括:
直接插入
折半插入
三、哪些算法最受初始序列影响
直接插入
非常受影响。
基本有序 -> 很快 O(n)
逆序 -> O(n²)
冒泡
也受影响。
带提前结束标志时:
已经有序 -> O(n)
快速排序
非常受pivot和初始序列影响。
如果固定选首元素:
基本有序 / 逆序
-> 可能退化到O(n²)
简单选择
几乎不受初始序列影响。
因为:
每趟都必须扫描整个未排序区间找最小
比较次数始终:
n(n-1)/2
堆排序、归并排序
时间复杂度较稳定:
都是O(n log n)
四、408场景选择秒杀
场景1:序列基本有序
优先:
直接插入
冒泡(带提前结束)
最典型:
直接插入
场景2:n很小
优先:
直接插入
原因:
实现简单
常数小
场景3:要求稳定
可考虑:
插入
冒泡
归并
基数
场景4:要求最坏仍O(n log n)
优先:
堆排序
归并排序
不要选快速排序。
场景5:要求额外空间尽量小
可考虑:
堆排序 O(1)
归并:
O(n)
空间更大。
场景6:大量数据在外存
优先:
归并思想
即:
外部排序 -> 多路归并
场景7:链表排序
更适合:
归并排序
因为链表不适合随机访问,但归并只需要顺序扫描。
五、比较排序的下界
如果排序算法只通过:
比较关键字大小
来确定顺序,那么平均意义下至少需要:
Ω(n log n)
级别的比较。
因此:
比较排序不可能普遍做到O(n)
但:
基数排序
计数排序
桶排序
不完全依赖关键字比较,所以可以突破这个限制。
六、快排、堆排、归并三大重点横向对比
|--------|------------|------------|------------|
| 对比 | 快速排序 | 堆排序 | 归并排序 |
| 平均时间 | O(n log n) | O(n log n) | O(n log n) |
| 最坏时间 | O(n²) | O(n log n) | O(n log n) |
| 空间 | 递归栈 | O(1) | O(n) |
| 稳定 | 否 | 否 | 是 |
| 初始序列影响 | 大 | 小 | 小 |
| 核心操作 | 划分 | 堆调整 | 合并 |
| 典型优势 | 平均很快 | 最坏稳定、空间小 | 稳定、适合外排 |
408里如果三者放一起比较,重点看:
稳定性
最坏复杂度
空间
七、交换次数和比较次数常考结论
简单选择排序
比较次数:
n(n-1)/2
与初始序列无关。
交换次数:
最多n-1次
冒泡排序
逆序情况下:
交换次数最多
交换次数等于:
逆序对数量
这是一个很重要的结论。
插入排序
元素移动量也和:
逆序对数量
高度相关。
序列越接近有序:
插入排序越快
八、堆排序的高频计算
1. 建大根堆
升序时:
父结点 >= 孩子
从最后一个非叶子结点开始向前调整。
若n个元素,从1开始编号:
最后一个非叶子结点 = floor(n/2)
所以建堆时:
i = floor(n/2), ..., 1
依次向下调整。
2. 每趟输出
升序排序:
堆顶最大
把:
堆顶 <-> 当前最后元素
交换。
然后:
堆大小减1
重新向下调整根
九、归并排序的高频计算
如果有n个元素,每次二路归并:
归并趟数 = ceil(log2 n)
例如:
n = 10
则:
ceil(log2 10) = 4
需要4趟二路归并。
每一趟处理全部n个元素,因此:
O(n log n)
十、外部排序:先理解为什么需要它
如果数据太大:
内存一次装不下
就只能:
磁盘 <-> 内存
反复读写。
这叫:
外部排序
外部排序最重要的成本不是CPU比较,而是:
磁盘I/O次数
因此目标:
尽量减少归并趟数
十一、外部排序标准流程
第一步:生成若干初始归并段
第二步:对这些归并段进行多路归并
第三步:不断归并直到只剩一个有序文件
十二、多路归并趟数公式
设:
r = 初始归并段数量
k = k路归并
则归并趟数约为:
ceil(log_k r)
例如:
16个初始归并段
2路归并
需要:
log2 16 = 4趟
如果改成:
4路归并
则:
log4 16 = 2趟
所以:
归并路数越大
归并趟数越少
I/O次数越少
十三、败者树是干什么的
多路归并时,每一轮都要从:
k个归并段当前元素
中找最小值。
如果每次普通扫描:
比较成本较高
败者树用于:
快速从k路中找当前最小元素
本质上:
减少CPU比较次数
注意:
败者树本身不会直接减少归并趟数
归并趟数主要取决于:
初始归并段数量r
归并路数k
十四、如何减少外部排序I/O
两个最重要方向:
方法1:增大归并路数k
k越大
log_k r越小
归并趟数越少
方法2:减少初始归并段数量r
也就是让:
每个初始归并段更长
常见技术:
置换-选择排序
它可以生成比内存容量更长的初始归并段。
十五、排序算法"第一反应"表
看到题目关键词,直接联想:
基本有序
-> 直接插入
每趟最大值到末尾
-> 冒泡
pivot归位
-> 快速
每趟选最小值
-> 简单选择
大根堆/小根堆
-> 堆排序
两个有序段合并
-> 归并
个位十位百位
-> 基数
外存、磁盘I/O
-> 多路归并
十六、稳定性怎么判断
判断一个排序是否稳定,只看:
两个关键字相同的元素
排序前后相对顺序会不会被颠倒
例如:
5a 3 5b
排序后如果:
3 5a 5b
稳定。
如果可能:
3 5b 5a
则不稳定。
十七、408选择题最常见陷阱
1. 快速排序最快,所以任何时候都最好
错。
最坏O(n²)
不稳定
递归需要额外空间
2. 折半插入排序是O(n log n)
错。
比较少了
移动仍然O(n²)
3. 堆排序稳定
错。
不稳定
4. 归并排序空间O(1)
错。
数组上的二路归并一般需要:
O(n)
辅助空间。
5. 简单选择排序在原序列有序时会变快
错。
比较次数基本不变。
6. 希尔排序稳定
错。
不稳定
十八、考前1分钟总表
直接插入:
稳定,O(n²),基本有序最好O(n)
折半插入:
稳定,比较少,整体仍O(n²)
希尔:
不稳定,原地
冒泡:
稳定,O(n²),最好O(n)
快速:
不稳定,平均O(nlogn),最坏O(n²),pivot归位
简单选择:
不稳定,始终O(n²),比较次数固定
堆:
不稳定,O(nlogn),O(1)空间
归并:
稳定,O(nlogn),O(n)空间
基数:
稳定,非比较排序,O(d(n+r))
十九、强化阶段你最需要练什么
排序这一章不建议平均用力。
优先级:
第一档:
快速排序
堆排序
归并排序
第二档:
直接插入
冒泡
简单选择
第三档:
希尔
基数
外部排序
每种算法至少练两类题:
1. 给初始序列,问一趟之后结果
2. 给性质,判断稳定性/复杂度/适用场景
二十、回去复习的位置
408数据结构
-> 第8章 排序
-> 8.1 基本概念
-> 插入排序
-> 交换排序
-> 选择排序
-> 归并排序
-> 基数排序
-> 外部排序
-> 排序算法分析与应用
最后真正要做到的是:
看到序列变化 -> 认算法
看到场景 -> 选算法
看到性质 -> 秒判断