引言
排序是算法里出场率最高的主题——因为它把「比较/交换/递归/分治/堆」这些底层思维全串起来了,也是面试与工程的双料基础。本文不做「背代码」,而是讲透十大排序的设计思路(谁在哪个环节赢)、稳定性与原地性(工程选型的隐藏约束)、复杂度对比,以及生产里真正的主角 TimSort 与 Introsort 为什么长那样。
前置:/others-big-o-complexity-guide/(复杂度分析)、/others-diff-patch/(LCS 与编辑脚本),并配合 计算机基础专题 的数据结构基础。
目录
- 1. 为什么排序是算法的万能磨刀石
- 2. 排序的三个关键属性:稳定性原地性适应性
- 3. O(n²) 家族:插入选择冒泡希尔
- 4. 分治双雄:快速排序与归并排序
- 5. 堆排序:利用堆的选择排序
- 6. 线性排序:计数桶基数
- 7. 工程排序:TimSort 与 Introsort
- 8. 稳定性在工程里的实际意义
- 9. 选型与复杂度对比表
- 10. 速查表与一句话记忆
- 延伸阅读
1. 为什么排序是算法的万能磨刀石
1.1 排序是无数问题的预处理
# 二分查找的前提是数组有序
# Top-K / 中位数 / 去重 / 归并依赖有序
# 大多数"找最值附近"的问题都能靠排序简化
# 数据库索引、MapReduce shuffle、流式 top-N 都是排序思想
1.2 排序能练到哪些底层能力
| 算法家族 | 练的思维 |
|---|---|
| 插入/希尔 | 局部有序、间隔优化 |
| 快排 | 分治、随机化、最坏情况分析 |
| 归并 | 分治合并、空间换时间 |
| 堆排序 | 堆结构、优先队列 |
| 计数/基数 | 用空间换时间、按位分解 |
记忆:排序是无数问题的预处理(二分/Top-K/去重的前提),也是分治/堆/稳定性的练兵场——练好排序就练好了一半算法思维。
2. 排序的三个关键属性:稳定性原地性适应性
2.1 稳定性:相等元素的相对顺序保不保留
稳定:相等元素保持原来的先后顺序
不稳定:相等元素可能交换相对位置
稳定性不是"质量",是"特性"——有些场景必须稳定(见 §8)
2.2 原地性:能不能在 O(1) 额外空间内完成
原地:额外空间 O(1)(如交换实现)
非原地:需要 O(n) 辅助空间(如归并的临时数组)
2.3 适应性:对"已基本有序"的数据快不快
自适应:输入越有序越快(插入排序在近有序时 O(n))
非自适应:复杂度与有序度无关
记忆:排序三属性——稳定性(相等元素顺序保不保留)、原地性(O(1) 额外空间)、适应性(对近有序数据快不快);选型看场景需求不看「哪个更高级」。
3. O(n²) 家族:插入选择冒泡希尔
3.1 插入排序:稳定、自适应、工程最爱
逐个把元素插到前方已有序区间的正确位置:
def insertion_sort(a):
for i in range(1, len(a)):
key, j = a[i], i - 1
while j >= 0 and a[j] > key:
a[j + 1] = a[j]; j -= 1
a[j + 1] = key
近有序时 O(n)——这就是工程排序(TimSort)拿它当小数组/尾巴加速的原因。
3.2 选择排序:交换次数最少但不稳定
每轮选最小放前面。交换 O(n) 次(适合写操作贵的场景),但不稳定、且无自适应。
3.3 冒泡排序:教学意义 > 实用价值
相邻两两交换,把大值「冒」到尾部。实现最直观,但常数大、无实际工程地位。
3.4 希尔排序:插入排序的间隔进化
按间隔(gap)跳跃地做插入排序,间隔递减到 1:
间隔序列(如 4,2,1 或 Knuth 序列 1,4,13,...)
每轮让"间隔为 gap 的子序列"有序 → 最终整体有序
复杂度约 O(n^1.3~1.5),非稳定
记忆:O(n²) 家族里插入排序是明星——稳定、自适应(近有序 O(n))、小数据快,工程里当「小数组快排」;选择交换少但不稳;冒泡只配教学;希尔用间隔把插入排序升维,非稳定。
4. 分治双雄:快速排序与归并排序
4.1 快速排序:分区 + 分治
选 pivot,把小于的放左、大于的放右,递归两半:
def quicksort(a):
if len(a) <= 1: return a
p = a[len(a)//2]
left = [x for x in a if x < p]
mid = [x for x in a if x == p]
right = [x for x in a if x > p]
return quicksort(left) + mid + quicksort(right)
- 平均 O(n log n),常数小,实际最快的通用比较排序
- 不稳定;最坏 O(n²)(已有序 + 选首元素当 pivot)
- 工程修复:随机化 pivot / 三数取中 / Introsort 自动转堆排
4.2 归并排序:稳定、可外部排序
把数组二分,排序两半再合并有序段:
def mergesort(a):
if len(a) <= 1: return a
m = len(a)//2
return merge(mergesort(a[:m]), mergesort(a[m:]))
- 稳定、O(n log n) 保证(无最坏退化)
- 非原地(O(n) 辅助空间)——但链表归并是原地且很优雅
- 外部排序(数据放磁盘)的基础:分段归并
记忆:快排是「实际最快」的通用排序(平均 O(n log n)、常数小)但不稳定、最坏 O(n²)——工程用随机化/三数取中/Introsort 兜底;归并稳定且无最坏退化,代价是 O(n) 空间,还是外部排序的根基。
5. 堆排序:利用堆的选择排序
5.1 思路:把「找最小」的扫描变成堆
每次从堆顶取最值,与末尾交换,重新下沉:
import heapq
def heapsort(a):
heapq.heapify(a)
return [heapq.heappop(a) for _ in range(len(a))]
- O(n log n) 保证、原地(O(1) 额外空间)
- 不稳定、常数大(实际比快排慢)
- 价值:需要「保证 O(n log n) 且原地」时(如嵌入式)唯一选择
5.2 与优先队列的关系
堆排序就是「反复从优先队列取最小」——同一个堆结构支撑了 Top-K、定时器、Dijkstra 等一大批工程场景。
记忆:堆排序 = 反复取堆顶的最值——O(n log n) 保证 + 原地,但不稳定且常数大;工程价值不是「快」,而是「既保证复杂度又原地」的兜底,以及支撑优先队列那套结构。
6. 线性排序:计数桶基数
6.1 计数排序:值域小时 O(n+k)
数每个值出现几次,再累加定位输出位置。稳定、O(n+k),只适合小值域整数。
6.2 桶排序:把数据撒进桶里
按范围分桶,桶内排序(常用插入),再合并。适合均匀分布的浮点数据。
6.3 基数排序:按位从低到高做稳定排序
按个位稳定排序 → 按十位稳定排序 → 按百位… → 最终有序
复杂度 O(d·(n+k)),d 是位数;前提是每轮稳定
6.4 什么时候用线性排序
# 计数:小值域整数(成绩/年龄/状态)
# 基数:固定位数的大数据(电话号码/ID)
# 桶:均匀分布连续值
# 关键前提:都不是通用比较排序,靠"值域/位数/分布"的额外信息换速度
记忆:线性排序用额外信息换速度——计数(小值域 O(n+k))、桶(均匀分布)、基数(按位稳定排序 O(d·(n+k)));都不是通用比较排序,用前确认值域/分布适配。
7. 工程排序:TimSort 与 Introsort
7.1 TimSort:Python/Java 标准库的真身
# 思路:识别数据里的"天然有序段"(run),合并这些段
# 小 run 用插入排序造出来;用类似归并的方式合并
# 结果:近有序数据接近 O(n)(自适应的极致)
# 且稳定——所以 Python 的 sorted 是稳定的
7.2 Introsort:C++ std::sort 的真身
# 思路:快排为主,递归过深(退化信号)自动切堆排序兜底
# 小数组切插入排序
# 结果:平均快排速度 + 最坏 O(n log n) 保证
# 注意:C++ std::sort 不保证稳定(要稳定用 std::stable_sort)
7.3 启示
# 现代标准库的排序都是"混合算法"——不迷信单一算法
# 稳定性与否是语言契约:Python sorted 稳定、C++ sort 不稳定
# 工程选择:你的数据有序度如何、稳定性要求如何,决定排序形态
记忆:工程排序是混合算法——TimSort 识别天然有序段+归并合并(近有序 O(n)、稳定,Python sorted 用它);Introsort 快排为主过深切堆排(最坏 O(n log n),C++ std::sort 用它但不保证稳定)。
8. 稳定性在工程里的实际意义
8.1 为什么稳定性不是「吹毛求疵」
多关键字排序依赖稳定性:先按次要关键字排,再按主要关键字排——次要关键字的有序性在稳定排序下被保留:
# 先按 age 排,再按 name 排(稳定)→ name 相同的人 age 仍有序
sorted(sorted(people, key=age), key=name)
8.2 不稳定会怎样
# 若第二次排序不稳定,name 相同的人 age 顺序被打乱
# 显示层:表格点列排序后,次级列顺序跳动
# 数据库:合并已排序结果集时稳定性影响「同值取谁」
# 结论:稳定与否是接口契约,不是实现细节
记忆:稳定性的实际意义是多关键字排序——先排次键再排主键时,稳定保证次键有序性被保留;排序稳定性是接口契约(Python sorted 稳定、C++ sort 不稳定),选错会在多键/合并场景出错。
9. 选型与复杂度对比表
9.1 复杂度总表
| 算法 | 平均 | 最坏 | 空间 | 稳定 | 原地 |
|---|---|---|---|---|---|
| 插入 | O(n²) | O(n²) | O(1) | ✓ | ✓ |
| 希尔 | O(n^1.3~1.5) | O(n^1.5) | O(1) | ✗ | ✓ |
| 选择 | O(n²) | O(n²) | O(1) | ✗ | ✓ |
| 冒泡 | O(n²) | O(n²) | O(1) | ✓ | ✓ |
| 快排 | O(n log n) | O(n²) | O(log n) | ✗ | ✓ |
| 归并 | O(n log n) | O(n log n) | O(n) | ✓ | ✗ |
| 堆排 | O(n log n) | O(n log n) | O(1) | ✗ | ✓ |
| 计数 | O(n+k) | O(n+k) | O(k) | ✓ | ✗ |
| 桶 | O(n) | O(n²) | O(n) | ✓ | ✗ |
| 基数 | O(d(n+k)) | O(d(n+k)) | O(n+k) | ✓ | ✗ |
9.2 选型决策
# 通用排序:直接用标准库(TimSort/Introsort),别手写
# 需要稳定:标准库稳定的那个(Python sorted / C++ stable_sort)
# 需要保证最坏 O(n log n) 且原地:堆排序
# 小数据/近有序:插入排序(或其混在工程排序里)
# 特殊数据分布:计数/桶/基数
# 面试必考但工程少见:手写快排/归并的边界与稳定性
记忆:选型看需求——通用用标准库混合排序(稳定选稳定版)、保证最坏+原地选堆排、近有序/小数据选插入、特殊分布选线性排序;工程上别手写通用排序。
10. 速查表与一句话记忆
| 算法 | 一句话 |
|---|---|
| 插入 | 稳定自适应,小数据/近有序之王 |
| 希尔 | 间隔插入,非稳定 |
| 选择 | 交换少但不稳 |
| 冒泡 | 教学意义为主 |
| 快排 | 实际最快,不稳、最坏 O(n²) |
| 归并 | 稳定无退化,非原地,外部排序根基 |
| 堆排 | 保证最坏+原地,不稳常数大 |
| 计数/桶/基数 | 用值域/分布/位数换速度 |
| TimSort | 识别有序段合并,Python 标配 |
| Introsort | 快排+堆兜底,C++ 标配 |
一句话记忆:排序的世界按「稳定性、原地性、适应性、复杂度」四维选型——通用场景直接上标准库(Python 的 TimSort 识别天然有序段、近有序 O(n) 且稳定;C++ 的 Introsort 快排为主过深切堆排、最坏 O(n log n) 但不稳定);手写排序里快排实际最快但最坏 O(n²)(工程用随机化/三数取中/兜底堆排),归并稳定无退化但非原地还是外部排序根基,堆排保证最坏复杂度又原地;线性排序(计数/桶/基数)靠值域/分布/位数的额外信息换 O(n) 速度;稳定性的实际意义在多关键字排序(先排次键再排主键,稳定保顺序)——「标准库别手写、稳定看契约、最坏有兜底、特殊分布走线性」是工程选型的四句真言。
延伸阅读
- /others-big-o-complexity-guide/ — Big-O 与复杂度分析
- /others-diff-patch/ — LCS 与编辑脚本(归并思想)
- /others-data-compression-guide/ — Huffman(优先队列与堆的应用)
- /others-fuzzy-text-matching/ — Top-K 与相似度排序
- 计算机基础专题 — 数据结构的系统学习
- TimSort 维基
- 可视算法排序演示
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。