刚上大学那门《数据结构》,排序算法大概是我背得最苦的部分——十来个算法,每个都要背代码、背复杂度、背稳定性,期末复习时,全靠“快速排序不稳定、堆排序不稳定、归并排序稳定”这种顺口溜撑着。结果考完不到半年,除了冒泡和快排,基本全还给老师了。直到后来工作,在处理千万级数据的排序性能、设计多字段排序逻辑时,才意识到当年那份“总结”之所以容易忘,是因为我只记住了结论,没理解排序算法背后的设计逻辑和适用边界。
这篇内容我把当年那份总结重新梳理了一遍,加上了这些年实战中的重新认识。不打算做成教科书式的算法罗列,而是围绕“怎么选型、为什么这样选、实际用起来有哪些坑”来展开。不管你是正在复习应付期末考或考研408,还是工作中需要处理数据排序,应该都能在里面找到点有用的东西。
1. 排序算法全景图:先建立一个“选型坐标系”
每次有同学让我推荐“最好的排序算法”,我都不知道怎么回答。排序算法领域里,没有银弹——有的算法对数据量敏感,有的对内存敏感,有的对初始有序度敏感。真正要先建立的是分类坐标系,然后看看每个算法落在坐标系的哪个位置。
1.1 分类维度:按比较方式、空间、稳定性三个轴切分
按最朴素的分类,排序算法先分成“比较类”和“非比较类”。
比较类算法的核心操作是两两比较元素的大小,然后根据比较结果决定是否需要交换或移动。冒泡、选择、插入、希尔、归并、快排、堆排序,全属于这一类。非比较类不直接比较元素大小,而是利用数据的特定位信息或数值本身的取值范围来完成排序,典型代表是计数排序、基数排序、桶排序。
第二个维度是空间消耗。原地排序算法只需要常数级别的额外空间,比如 O(1);而非原地排序需要 O(log n)、O(n) 甚至更大的辅助空间。归并排序需要 O(n) 的辅助数组,经典快排的递归调用会消耗 O(log n) 的栈空间,这些都直接影响大数据场景下的可用性。
第三个维度是稳定性。这个我们后面专门展开,先记住一个判断线索:如果排序后相等元素的相对顺序不发生改变,算法就是稳定的;否则就是不稳定的。
1.2 一张表看懂主流算法的基础定位
| 算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|
| 冒泡排序 | O(n²) | O(n²) | O(1) | 稳定 |
| 选择排序 | O(n²) | O(n²) | O(1) | 不稳定 |
| 插入排序 | O(n²) | O(n²) | O(1) | 稳定 |
| 希尔排序 | O(n^1.3~n²) | O(n²) | O(1) | 不稳定 |
| 归并排序 | O(n log n) | O(n log n) | O(n) | 稳定 |
| 快速排序 | O(n log n) | O(n²) | O(log n) | 不稳定 |
| 堆排序 | O(n log n) | O(n log n) | O(1) | 不稳定 |
| 计数排序 | O(n+k) | O(n+k) | O(k) | 稳定 |
| 基数排序 | O(d(n+k)) | O(d(n+k)) | O(n+k) | 稳定 |
| 桶排序 | O(n+k) | O(n²) | O(n+k) | 稳定 |
这张表本身不难背,但要想真正把排序算法学扎实,不能停在“背表格”这个层面。比如希尔排序的平均复杂度,到现在学术界都没有一个统一的精确表达式,不同增量序列得到的结果差异很大。再比如快排虽然最坏是 O(n²),但通过随机化和小区间优化,实际工程里几乎不会碰到底。这就是“理论复杂度”和“工程表现”之间第一条裂缝。
1.3 比较类排序的理论天花板:为什么是 n log n
很多人学排序时有这个疑惑:为什么所有基于比较的算法,平均复杂度都逃不出 O(n log n)?稍微深挖一点就会发现,这不是某种巧合,而是一个信息论意义上的约束。
假设有 n 个互不相同的元素,它们的排列方式有 n! 种。排序过程每做一次比较,最多能把当前的可行排列空间一分为二。k 次比较最多能区分出 2^k 种结果,所以要区分 n! 种排列,必须满足:
2^k ≥ n!
两边取对数,k ≥ log₂(n!) ≈ n log₂ n - 1.44n。也就是说,任何基于比较的排序算法,在最坏情况下至少需要进行 n log n 数量级的比较。快速排序、归并排序在渐进意义下已经到达了这个下界,所以它们被称为“渐近最优的比较排序算法”。
这层理解有什么实际意义呢?它会直接指导你一个判断:如果某天遇到一个号称“O(n) 时间完成通用排序”的方案,第一反应不应该是惊喜,而应该怀疑它是不是偷偷利用了数据的某种特殊性质。真实世界里不存在免费的午餐,比较排序的时间下界是物理性约束。
1.4 非比较排序的突破口:用数据本身的“位置”说话
非比较排序能突破 n log n 的下界,靠的不是更巧妙的比较策略,而是跳过了“比较”这个操作本身。拿计数排序举例,它要求数据范围已知且不大。假设要排序 10 万个人的年龄,年龄范围是 0~150,只需要开一个长度为 151 的计数数组。第一遍遍历统计每个年龄的人数,第二遍根据计数数组把数据放回目标位置,时间复杂度 O(n+k),其中 k 是数值范围。
这个思路的本质,是把“元素之间的大小比较”转换成“数值到数组下标的映射”。理解了这一点,就明白了计数排序为什么要求数据必须是整数,为什么数值范围不能太大——数组下标能表示的数值范围天然有限。
基数排序则是计数排序在多位场景下的延伸。它从最低位开始,对每一位都做一次稳定的“桶分配”,经过 d 轮后,整个序列有序。这里有个关键点容易被忽略:每一轮桶分配使用的底层排序,必须是一个稳定排序,否则跨轮次累积的顺序会错乱。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 稳定性这道分水岭:既是考试重点,也是工程漏坑点
稳定性这个概念,在大学的排序章节里往往只占一小段,期末考试最多考几道判断题。但真正做业务开发时,稳定性带来的影响比你想象的更隐蔽。
2.1 先搞清楚“稳定”到底有什么用
一个排序算法是稳定的,意思是对于值相等的关键字,在排序前后的相对次序保持原有顺序。这里“原有顺序”指的往往是输入数据的原始顺序。
最经典的需求场景:一个电商订单列表,先按“下单时间”排序,再按“订单金额”排序。如果第二趟排序使用的算法不稳定,那么相同金额的订单之间,下单时间顺序就可能被打乱。而如果第二趟排序是稳定的,那么相同金额的订单天然会保持第一趟按时间排好的顺序。事后来看,用户想要的其实是“金额从大到小,金额相同再按时间前后”——用稳定排序只需要倒过来做两趟排序,就能不写复杂的复合比较器。
这就是稳定性的价值:它允许你把多个排序条件拆成多轮独立的排序,每轮使用稳定的算法,最终结果完全符合复合条件。如果算法不稳定,写复合比较器的时候就必须一次性把全部门道写进去,代码复杂度和出错概率都会上升。
2.2 为什么有的稳定有的不稳定:跳跃交换是根源
判断一个算法稳定性如何,最靠谱的方式不是背结论,而是看它的核心交换动作是“相邻交换”还是“跳跃交换”。
冒泡排序和插入排序之所以稳定,是因为它们只让元素和相邻位置的元素进行交换。当一个元素“越过”另一个值相等的元素时,它们中间必然隔着一次相邻位置互换,而这种互换严格遵守“严格大于才动,等于不动”的规则,所以相等元素的相对次序不会被破坏。
归并排序稳定,是因为它的核心操作是合并两个有序子序列。算法在左右两个指针所指元素相等时,规定先取左序列的元素。于是,所有左序列中与右序列相等的元素,都会先于右序列被输出,相对次序得到保留。
不稳定阵营的解释同样清晰。选择排序之所以不稳定,是因为它每一轮选出最小元素后,会和当前轮起始位置的元素做一次远距离交换;如果这两个位置之间恰好有与“被交换元素”相等的值,整体次序就乱了。快速排序的 partition 过程用的是左右指针快速跨越的交换;堆排序的建堆和调整过程,父子节点的交换也是远距离的。这些跳跃式交换,一碰上相等元素就很可能改变相对顺序。
2.3 基数的关键推论:底层的桶分配必须稳定
前面提到基数排序每一轮桶分配要依赖稳定排序,原理在于它处理的是多关键字。假设有两个数字 13 和 23,先按个位分配,两者都进入桶 3;再按十位分配,两者又都进入桶 2。这样十位相同的前提下,它们相对次序由个位那轮决定——而个位那轮正好保持了原来的输入次序,所以 13 排在 23 前面,结果正确。
如果某一轮用的是不稳定排序,数字的原始次序丢失,同关键字下的先后顺序就会变得不可预测,最终排序结果可能完全错误。所以基数排序的教科书实现里,桶内连接或收集时通常采用稳定的计数排序或队列方式,严格保证“从低位到高位逐轮有序”。
2.4 工程视角:什么时候可以“放弃稳定”
也不是所有场景都非稳定排序不可。最典型的例子:如果你只对一组整数排序,那稳定性没有任何意义,两个数值相等但“身份”不同的整数并不存在,谁先谁后无人在意。排序对象的“相等”是否代表可区分的实体,是判断稳定性有没有价值的前提。
实际工程里分配排序任务时,我通常用这个小原则:如果排序对象是原始数据类型(int、float、字符串等),优先考虑性能和空间,稳定性不是约束条件;如果排序对象是结构体、对象或带业务含义的元组,尤其是多轮排序场景,稳定性往往是必须满足的硬指标。
3. 快速排序:为什么它是通用默认,又有哪些致命软肋
如果只允许掌握一种通用排序算法,绝大多数人会选快速排序。它既是考研手撕代码的高频考点,也是 C++ 的 std::sort、Java 标准库排序的核心设计基础。但快排从来不完美,它的性能优势建立在数据分布和实现细节之上。
3.1 平均复杂度的直觉推导:两个“一半”的力量
快排的核心是分治:选一个基准元素 pivot,把数组划分成小于等于和大于等于 pivot 的两部分,然后递归处理子数组。划分操作是 O(n) 的线性扫描,递归过程把数组一分为二。如果每次划分都恰好把数组分成规模接近的两半,那么递归的深度是 O(log n),每层总工作量累计是 O(n),整体就是 O(n log n)。
如果每次划分极端不均衡,比如数组已经正序,而选基准的策略固定取第一个元素,那么划分出来的两个子数组规模是 0 和 n-1。递归深度退化为 O(n),整体复杂度退化到 O(n²)。空间复杂度也同步退化——递归栈的深度会从 O(log n) 变为 O(n)。在大数据量的场景下,这不止是“慢”的问题,还有可能直接触发栈溢出。
正经的复杂度推导可以这样理解:设 T(n) 为快排排序 n 个元素的时间,划分开销为 cn。当划分均衡时,T(n)=2T(n/2)+cn,解得 T(n)=O(n log n);当划分极端不均衡时,T(n)=T(n-1)+cn,解得 T(n)=O(n²)。
3.2 数据分布如何“杀死”快排:有序、逆序、重复元素
我实测过一个典型的极端场景:用基本版快排(固定取第一个元素作为 pivot)去排序一个已经有序的 100 万元素数组,运行时间比排序随机数组长了两三个数量级,且递归深度深到接近栈溢出。
逆序数组原理相同,因为每次划分仍然极度不均衡。还有一个更隐蔽的杀手场景——大量重复元素。经典的 Lomuto 或 Hoare 划分在处理“全部元素都等于 pivot”的数组时,容易退化成每次只能划掉一个元素的两段式划分,复杂度趋向 O(n²)。解决方案是“三路划分”,也就是把数组分成小于、等于、大于 pivot 的三个区段,中间相等区段不再递归处理。这样重复数据越多的场景,三路快排表现反而越好。
3.3 教科书快排与工业级快排的差距
教科书为了教学清晰,往往使用最简单的 Lomuto 划分、取末尾元素做 pivot 的写法。但这版工程上有不少隐患,工业级实现会一个个消掉。C++ 标准库里常见的做法是:首先用“三数取中”策略选 pivot,也就是取首、中、尾三个位置的中间值,这样基本避免了全序或逆序退化;递归到子数组规模小于阈值(比如 16)时改用插入排序,因为小规模下插入排序的常数极小,递归开销反而占大头;对递归深度特别深的情况还会转用堆排序作为兜底,保证整体最坏复杂度是 O(n log n)。
以下是教科书版快排和工程优化版快排的一个核心差异对比:
| 项目 | 教科书版本 | 工程优化版本 |
|---|---|---|
| pivot 选择 | 固定取首/末元素 | 三数取中或随机化 |
| 小规模子数组 | 仍递归快排 | 转插入排序 |
| 重复元素处理 | 常规两路划分 | 三路划分/Segmented sort |
| 最坏深度缓解 | 无 | 转堆排序兜底 |
| 递归实现 | 标准递归 | 尾递归优化/显式栈 |
这段对比就是“算法理论”与“可用算法”之间巨大差距的浓缩。考试可以只懂教科书版,但要真正把快排用在生产环境,至少要知道这些优化维度。
3.4 递归深度的现实问题:栈空间比时间更危险
快排最容易被初学者忽略的点是它隐藏在“常数级额外空间”描述背后的递归消耗。理论分析说空间复杂度 O(log n),指的是平局情况下递归栈的深度。一旦发生退化,栈深度就变成 O(n)。顺便说一句,长时间跑递归程序的进程往往需要设置较大的线程栈,否则会在排序很大数组时栈溢出崩溃。
工程上常见的防御手段是把递归改成显式栈的迭代实现。好消息是快排的迭代版本并不复杂,而且能精确控制栈大小。很多分布式框架处理大规模数据时对单节点内存和栈有严格限制,这版“显式栈快排”就有用武之地。
下面给出一个偏工程化、兼顾可读性的 C 语言版快排骨架,采用三数取中、对小幅子数组转插入排序的思路:
c复制// 三数取中:返回首/中/尾三者中位数所在位置的下标
static int median_of_three(int arr[], int left, int right) {
int mid = left + (right - left) / 2;
int a = arr[left], b = arr[mid], c = arr[right];
if (a > b) { int t = a; a = b; b = t; }
if (a > c) { int t = a; a = c; c = t; }
if (b > c) { int t = b; b = c; c = t; }
// 现在 a <= b <= c,b 是中位数
if (arr[left] == b) return left;
if (arr[mid] == b) return mid;
return right;
}
static void insertion_sort(int arr[], int left, int right) {
for (int i = left + 1; i <= right; i++) {
int key = arr[i];
int j = i - 1;
while (j >= left && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
void quick_sort(int arr[], int left, int right) {
while (left < right) {
if (right - left + 1 <= 16) {
insertion_sort(arr, left, right);
return;
}
int pivot_idx = median_of_three(arr, left, right);
int pivot = arr[pivot_idx];
// 把 pivot 交换到最右端,然后做标准 Lomuto 划分
int tmp = arr[pivot_idx]; arr[pivot_idx] = arr[right]; arr[right] = tmp;
int store = left;
for (int i = left; i < right; i++) {
if (arr[i] < pivot) {
int t = arr[store]; arr[store] = arr[i]; arr[i] = t;
store++;
}
}
int t = arr[store]; arr[store] = arr[right]; arr[right] = t;
// 尾递归优先处理较长的段,较短段入迭代
if (store - left < right - store) {
quick_sort(arr, store + 1, right);
right = store - 1;
} else {
quick_sort(arr, left, store - 1);
left = store + 1;
}
}
}
这个小优化思路值得说一句:把递归调用范围控制在较短的子数组上,就能把递归深度约束在 log n 数量级附近。
4. 容易被低估的算法:堆排序、希尔排序和线性时间排序的边界
除了快排,还有一个“班上存在感不高但偶尔一击致命”的排序算法群体。期末复习时它们占的分值不高,但选型时它们往往是最优解。
4.1 堆排序的谜之尴尬:性能上限很高,实际用得很少
堆排序的时间复杂度稳定在 O(n log n),空间复杂度是 O(1),理论上是相当优秀的原地比较排序算法。但如果你统计工作环境里代码库中用到的排序实现,堆排序出现的概率远低于快排和归并。原因是堆排序虽然渐进复杂度优秀,但常数因子很大。建堆过程是 O(n) 的,但后续每一轮取出最大值并调整堆,要执行 log n 次父子比较和交换。这些操作访问的内存地址跨度很大(父节点到子节点下标通常相差数倍),缓存命中率极低。相比之下,快排在一段连续内存上的扫描更符合现代 CPU 的硬件特性。
堆排序真正大放异彩的场景是“不完全排序”。比如在十亿个数里取前 100 个最大数,堆排序可以直接用大小为 100 的小顶堆在线性时间内完成,而不需要对整个数据集全盘排序。这就是“Top K 问题”的标准解法。
4.2 希尔排序:比插入排序快,但增量序列是个玄学
希尔排序在“数据结构”课程里通常被介绍为“优化的插入排序”。它先把间隔较小的元素做插入排序,逐步缩小间隔,最终间隔为 1 时就是标准的插入排序。因为前面几轮把序列变得大致有序,最后一轮插入排序的移动次数大幅减少,总时间降到远低于 O(n²)。
希尔排序复杂度之所以很难精确描述,是因为它和间隔序列的选取强相关。用最原始的 Hibbard 序列等设计,最坏可以做到 O(n^1.5);用 Sedgewick 序列能让平均接近 O(n^1.3)。工程上希尔排序的商业价值不大,因为大部分通用排序场景被快排和归并覆盖了,但它有一个优点在嵌入式或内存极小场景很难被替代:它完全原地排序且不需要递归,栈空间不仅是 O(1),还是严格的“零额外分配”。
4.3 线性时间排序的适用铁律:范围、整数、空间换时间
计数排序、基数排序、桶排序能在线性时间内解决问题,但每条都有硬性场景前提。
计数排序的第一条铁律是数据必须是整数且范围可预估。如果数据是浮点数、字符串或者业务对象,计数排序无从下手。第二条铁律是空间消耗不能忽略。如果数据范围是 0 到 10^9,就算只有 100 个数,计数排序也得开 10 亿个计数器,直接爆内存。这就像单次阅卷统计分数时成绩只有 0~150 分,用 151 个筐很合理;但统计全国身份证号时就完全不可行,因为分布空间太巨大。
桶排序应用的核心是“分桶策略”。它的技巧在于桶的每个单元内部还要再做排序,如果单桶内数据分布仍不均匀,性能会急剧退化。比如对一组符合均匀分布的浮点数做桶排序,性能接近 O(n);但如果数据极其集中,所有数都落入同一个桶,退化成 O(n²)。所以桶排序工程上往往和“对数据分布有预判”的专项场景绑定。
基数排序的空间和适用范围比计数排序宽松,只要数据能被拆分成固定位数的关键字即可,不一定非要是数值。字符串排序也能用,从右往左逐字符做稳定桶排序即可。但它的单趟开销不能只看 n,还得看关键字的位数 d,实际复杂度是 O(d(n+k))。
4.4 真实世界的数据大多“近乎有序”:TimSort 的巧妙
现代语言标准库里一个越来越常见的默认排序是 TimSort,它最早用于 Python 的列表排序,后来被 Java 的 TimSort 实现、安卓系统、以及很多大数据框架采用。
TimSort 的核心思想非常贴合真实世界的数据特点:检测数据中已有的有序片段(运行段,run),把这些 run 用归并的方式逐段合并;对长度小于阈值的 run 先做二分插入排序,再进入归并过程。因为现实中的数据往往不是完全随机的——比如数据库查询结果、日志时间序列、用户操作记录——天然带有大量局部有序性,TimSort 利用这一点,在近乎有序的数据上可以把复杂度降到接近 O(n)。
这也解释了为什么很多排序算法“平均复杂度一样”,但实际表现差距巨大。分析时只看元素规模 n,但常数因子和“对数据分布的先验假设”决定了真实场景下的天壤之别。
5. 选型即决策:从数据库到业务代码,排序到底该交给谁
和“手撕排序”的考试场景不同,真实开发里的绝大多数情况,排序能力早就被语言标准库和数据库封装好了。你真正要做的不是自己写快排,而是知道在什么场景下该调用哪一层能力,以及当默认行为不满足需求时,问题出在哪里。
5.1 语言标准库的选择,已经替你做了很多权衡
C++ 的 std::sort,核心是优化过的快速排序(introsort),绝大多数情况下你不需要自己再写快排。Java 的 Arrays.sort 基本类型时用 Dual-PivotQuickSort;排序对象数组时则使用 TimSort。Python 的 list.sort 也是 TimSort 的稳定实现。Rust 的 sort_unstable 基于模式销毁快排(pattern-defeating quicksort,pdqsort),sort 则实现了一种稳定的归并式排序。
这些默认实现背后是同一个逻辑:在“时间复杂度”“稳定性”“缓存友好度”“现实数据分布”四者之间做权衡。追求极致速度、愿意牺牲稳定性,就去快排家族;追求稳健和稳定,就用归并家族(TimSort 是它的变体)。
5.2 数据量、稳定性、内存、原始有序度,四个维度坐定选型
下面这组选择思路是我个人的实际操作经验,适合绝大多数通用排序任务:
| 环境条件 | 推荐算法 | 理由 |
|---|---|---|
| 数据几乎有序(日志、增量数据) | 插入排序或 TimSort | 近乎 O(n) 的复杂度 |
| 通用型大数据排序、内存紧张 | 快排(工程优化版) | 平均性能最好,缓存友好 |
| 要求绝对稳定、可多轮排序 | 归并排序/TimSort | 稳定性有保证 |
| Top K 问题(最大/最小 K 个) | 堆 | 时间复杂度 O(n log k) |
| 小数据量(少于 50 个元素) | 插入排序 | 常数极小,实现简单 |
| 整数范围有限的排序 | 计数排序 | 线性时间,代价是额外空间 |
| 多位关键字或字符串 | 基数排序 | 线性时间,稳定 |
这里有个容易被忽略的细节:当数组长度小于阈值时,插入排序几乎总是打赢所有“高级算法”。因为高级算法的常数因子和函数调用开销在那个规模下反而比简单算法更大,这是语言标准库普遍做“切换到插入排序”优化的重要原因。
5.3 业务代码里排序经常是“别人的能力”:数据库和高性能组件
大多数业务系统的排序,真正落到应用层代码之前,在数据库里已经完成了一大部分。SQL 的 ORDER BY 背后,是数据库执行计划里的排序算子。你唯一需要掌握的是:怎么写出能用上索引的排序条件,以及多字段排序时怎么通过稳定排序的组织方式表达业务规则。
另一大类场景是内存型数据组织,比如 Redis 的有序集合、Elasticsearch 的排序查询、ClickHouse 的 ORDER BY。这些系统底层全都有自己高度定制的排序和归并策略。你遇到的大部分“排序问题”,本质上是“排序条件表达问题”,而不是“排序算法实现问题”。
5.4 我在业务代码里踩过的排序坑
排序相关的坑,主要集中在比较器的边界条件上。最常见的是“比较器返回值超越 int 范围”。如果两个 long 值相减后直接作为比较器返回值,一旦差值超过 int 上限,返回值溢出会导致排序结果错乱。安全的写法是用逻辑判断返回 -1/0/1,而不是做减法。
第二个坑是“比较器的一致性”。Java 里要求比较器满足传递性,否则 TimSort 会在运行时抛“Comparison method violates its general contract”异常。我在一个项目里就遇到过——排序规则里对 null 值和特殊状态的处理顺序定义不完整,导致同一条数据在不同上下文里的比较结果矛盾,整个列表排序直接崩溃。
第三个坑是“排序开销被低估”。排序比较器如果内含复杂计算(正则、数据库查询、字符串格式化),数据量一大时长会成倍放大。最稳妥的方法是把待比较的值提前提取成低成本的字段或预计算好再排序,宁可多占点内存,也不能把重量级逻辑重复丢进比较器里。
第四个坑是“多列排序时直接写复合比较器,忽略了稳定性利用”。前面我们说过,稳定排序至少给了你把复合排序拆成“多轮排序”的备选空间。有些语言和框架里,复合比较器写起来很别扭,拆成多轮稳定排序反而清晰、快速、不容易出错。
写在最后:排序算法值得“学慢一点”
回顾我自己的实践经历,排序算法不是靠考前突击背下来的,而是在多次真实使用后才真正理解。我对它们的认识提升,恰好发生在几个节点:第一次用堆解 Top K 问题,第一次调通 TimSort 的复杂对象排序,第一次在千万级数据集上验证快排退化及其优化效果。每一次理解加深的前提,都是我在一个具体的场景里反复经历了“为什么不用别的算法”的思考。
排序算法这片地,线性算法也好,分治思想也罢,最终积累下来的不是代码片段,而是一套关于权衡的判断力——时间复杂度和空间复杂度之前怎么权衡,平均表现和最坏表现之间怎么取舍,算法特性和数据分布之间怎么匹配。这种判断力,才是这门课真正想留给你的东西。学得慢一点,把它当成建立体系的机会,后面用到时会有意想不到的轻松感。
