排序这玩意儿,我做了十几年开发,回头看,它才是数据结构里最“值钱”的基础课。你刷题可能觉得排序简单,但实际项目里,从数据库SQL的ORDER BY,到前端点击表头排序,再到推荐系统的热度排序,全是它的影子。这篇就把“初识基本排序”这件事讲透,不仅讲算法本身,更讲清楚每个选择背后的“为什么”,以及我在真实项目里踩过的那些坑。
1. 排序到底在解决什么问题——先把“为什么”讲透
很多初学者觉得排序就是“把数字从小到大排”,这个理解太窄了。排序本质上是建立一种可预测的次序,让后续的查找、统计、去重、展示都变得高效。你想想,一个无序数组里找一个数,平均要遍历一半的数据,但排好序之后就能二分查找,效率从O(n)降到O(log n),这个差距在百万级数据上就是几秒和零点几毫秒的区别。
1.1 排序在真实场景里无处不在
我随便举几个日常开发的例子,你感受一下:
- 电商首页的商品列表,按照综合评分、销量、价格、上架时间排序,这背后就是多关键字的排序组合。
- 数据库执行
SELECT * FROM orders ORDER BY create_time DESC时,如果create_time字段有索引,MySQL可以直接倒序遍历索引,避免文件排序;如果没有索引,就得先把结果集拿到临时表里做排序,代价翻倍。 - 前端表格组件,用户点一下表头,列表就按那一列排序。脚本里就是调用JavaScript的
Array.prototype.sort()加一个自定义比较函数。 - 游戏MOD管理器(类似MO2)里左侧的加载顺序列表,本质上就是一个自定义规则的字符串排序。
你用这些场景去理解排序,就不会觉得它只是个考试题目了。
1.2 一个排序问题的标准定义与核心指标
任何一个排序问题,第一步都是想清楚三件事:输入是什么类型(整数、字符串、对象)、输出要什么顺序(升序、降序、自定义规则)、有没有额外约束(稳定性、内存限制)。然后我们才谈算法选择。
评价排序算法好坏,有四个核心指标,这比背代码重要得多:
| 指标 | 含义 | 为什么重要 |
|---|---|---|
| 时间复杂度 | 数据规模N增长时,操作次数的增长趋势 | 决定了大数量下的性能上限 |
| 空间复杂度 | 排序过程中额外使用的内存 | 决定能否在内存受限的环境跑 |
| 稳定性 | 相同关键字的元素,排序后相对次序是否保持不变 | 数据库和排行榜非常需要 |
| 原地性 | 是否在原数组上直接操作 | 避免频繁分配内存,对性能影响巨大 |
稳定性这个东西,新手最容易忽略。举一个场景:订单列表先按下单时间排序,再按用户ID排序。如果第二次排序是稳定排序,那么同一个用户的多笔订单会保持时间顺序;如果是不稳定排序,时间顺序就乱掉了。这也是为什么Java的Collections.sort()用的归并排序,因为它是稳定的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 十一种常见排序算法的分类与设计思路
网上一搜“八大排序”或者“排序算法”,能搜出一大堆。我建议你学习的时候,先不要一头扎进代码,而是先把算法按“思想流派”分类,这样理解起来是有体系的。
2.1 比较排序与非比较排序的底层逻辑差异
比较排序,就是通过两两比较元素的大小来决定次序,比如冒泡、选择、插入、希尔、归并、快排、堆排。理论上有个下界:任何基于比较的排序,最坏情况下时间复杂度不可能低于O(n log n)。这是因为每次比较最多产生两种结果,n个元素的排列有n!种可能,需要的比较次数至少是log2(n!)≈n log n。
非比较排序,比如计数排序、基数排序、桶排序,它们不靠比较,而是靠元素本身的“数值范围”或者“位”来直接放位置,所以可以突破O(n log n)的下界。但它们有很强的前置条件:元素必须是整数或可以映射成有限范围内的整数,而且范围不能太大。
2.2 时间复杂度的几个关键分水岭
我整理了一张速查表,你对照着看:
| 算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|
| 冒泡排序 | O(n²) | O(n²) | O(1) | 稳定 |
| 选择排序 | O(n²) | O(n²) | O(1) | 不稳定 |
| 插入排序 | O(n²) | O(n²) | O(1) | 稳定 |
| 希尔排序 | O(n^1.3~1.5) | 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) | 稳定 |
你注意看,没有任何一个算法在所有维度上都最优。快排平均最快但最坏退化;归并稳定但占额外空间;堆排原地省空间但缓存不友好。这就是为什么实际工程中要混合使用多种策略,比如C++的std::sort就是快排+插入排序+堆排序的混合体。
2.3 处理边界数据的思路:小规模数据与大数据的策略分歧
很多人在面试时会忽略一个细节:插入排序在数据规模很小(比如小于16个元素)的时候,实际速度比快排还快。原因是快排的递归调用和分区操作有额外开销,而插入排序的常数极小。所以你看std::sort的实现,当递归到子区间长度小于阈值时,会直接改用插入排序收尾。
这个思想特别重要,它告诉我们:算法的选择不是绝对的,要结合数据规模、数据特征、硬件环境综合判断。我参与过一个实时榜单项目,数据量不大但更新极其频繁,最后用的就是插入排序的变体——维护一个有序的小数组,新数据来了直接插入,而不是每次全量排序,效果立竿见影。
3. 核心细节解析与实操要点:从代码里踩出来的坑
只看理论不写代码,等于没学。但直接把代码背下来,也不一定能写对。我见过太多人在基础排序上栽跟头,下面把每个算法最容易出错的地方单独拎出来说。
3.1 选择排序的“最小元素交换”陷阱
选择排序的核心思路是每一轮从未排序区间里找出最小值,放到已排序区间的末尾。逻辑很简单,代码很容易写对,但有一个细节常常被忽略:当最小元素正好在目标位置时,交换是无意义的,甚至可能出问题。
看一下这段C++代码:
cpp复制void selectionSort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
int minIdx = i;
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIdx]) {
minIdx = j;
}
}
// 只有minIdx != i时才需要交换
if (minIdx != i) {
std::swap(arr[i], arr[minIdx]);
}
}
}
如果不加minIdx != i的判断,在元素是自定义对象的时候,交换操作可能触发不必要的拷贝构造和析构,造成性能下降。另外,选择排序是不稳定的,因为交换可能把后面的相同元素换到前面去。如果你的业务场景要求稳定排序,用选择排序就是自找麻烦。
3.2 插入排序的边界处理与“几乎有序”场景
插入排序的思路是像打扑克牌一样,把新元素往已经排好序的序列里插。它的优势在于,对于“几乎有序”的数据,插入排序的时间复杂度可以降到O(n),因为内部循环几乎不执行。
写的时候最容易错的是边界条件。我之前见过一个实习生写的:
cpp复制void insertionSort(int arr[], int n) {
for (int i = 1; i < n; i++) {
int key = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > key) { // 边界容易写错
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
这个版本是对的。但如果你把while条件写成arr[j] > key && j >= 0,看起来没错,实际上是先访问arr[j]再判断j是否越界,等于数组下标负一,直接未定义行为,程序可能崩溃也可能默默出错。必须先判断j >= 0,再判断arr[j] > key,顺序不能反。
3.3 希尔排序的步长序列选择
希尔排序是插入排序的改进版,通过不断缩小的步长进行分组插入排序,让大跨度的元素先归位,最后步长为1时变成普通插入排序。它的代码其实不复杂,但步长序列的选择直接决定性能。
最常见的步长序列是n/2, n/4, ..., 1,也叫希尔增量。这个序列在最坏情况下复杂度是O(n²)。如果换成Hibbard增量(1, 3, 7, 15...),最坏情况可以到O(n^1.5)。实际工程中,还有人用Sedgewick增量,效果更好。
我的建议是:如果你不搞学术研究,直接用gap = gap / 2就够用,代码清晰、容易理解,性能也还过得去。非要追求极致性能,工程上一般直接上快排或归并了,不会选希尔。
3.4 快速排序的退化场景、栈溢出与基准值选择
快排是应用最广的排序算法,但也是坑最多的一个。它最经典的坑有三个:退化、递归深度、基准值选择。
先说退化。如果每次选的基准值都是当前区间的最小值或最大值,那么分区极不平衡,时间复杂度退化成O(n²)。最经典的退化场景是:对一个已经有序的数组,每次选第一个元素作为基准值。排序有序数组反而成了最坏情况,这个反直觉的点必须记住。
再说递归深度。在最坏情况下,递归深度是O(n),对于百万级数据,递归栈可能溢出,程序直接崩。解决方法是尾递归优化:先递归处理较短的那一半,然后用循环处理较长的一半,这样递归深度最多O(log n)。
最后说基准值的选择。最简单的是取第一个元素,但有序数组会退化。改进方案有三种:
- 随机选基准:平均性能好,但随机数生成有开销。
- 三数取中:取区间首、中、尾三个元素的中位数作为基准,能有效避免有序数组退化。
- 双基准:Java的
Arrays.sort()对基本类型用的是双基准快排,对对象用的是TimSort,都是工业级优化。
我在实际项目中一般用三数取中,既不需要随机数,又能稳定应对大多数场景。
3.5 归并排序的空间复杂度与链表场景
归并排序的思路是分而治之:把数组分成两半,分别排序,再合并。它的时间复杂度稳定在O(n log n),而且稳定,但代价是需要O(n)的额外空间。
新手写归并排序,最容易错的是合并时的边界条件。我建议你写的时候统一用“左闭右开”区间,即[left, right),这样很多边界判断都会简化。还有一个优化技巧:当待合并的两个子数组已经有序时,可以跳过合并。
归并排序还有一个特殊应用:链表排序。因为链表不支持随机访问,快排在链表上效率极差,而归并排序只需要顺序遍历,非常适合链表。这也是为什么很多语言标准库的链表排序用归并实现。
3.6 堆排序的不稳定性与建堆细节
堆排序利用完全二叉树的特性,通过堆这种数据结构来排序。它的空间复杂度是O(1),时间复杂度稳定在O(n log n)。但它是不稳定的,而且实际运行速度往往比快排慢,因为对缓存不友好。
写堆排序最容易错的地方是:堆调整时,子节点下标的计算。以0为起始下标,左孩子是2*i+1,右孩子是2*i+2;以1为起始下标,左孩子是2*i,右孩子是2*i+1。前后千万别搞混。我建议你统一用0起始,写起来直观。
还要注意建堆和排序的方向。建堆是从最后一个非叶子节点开始,向上做下沉操作;排序是把堆顶元素和最后一个元素交换,然后把剩余元素重新调整成堆。这个方向反了,结果就不对。
3.7 计数排序和基数排序的前置条件
计数排序的思路是,如果知道元素的最大值k,就开一个长度为k+1的数组,遍历一遍原始数据,统计每个值出现的次数,然后按顺序输出。时间复杂度O(n+k),空间复杂度O(k)。它的局限性很明显:只能排非负整数,而且k不能太大。如果k是10^9,你开一个这么大的数组,直接内存爆掉。
基数排序是计数排序的扩展,按位依次排序。它有两种方向:LSD(从低位到高位)和MSD(从高位到低位)。LSD通常需要用稳定排序作为子过程,否则同一位的次序会乱。基数排序适合给字符串或者定长整数排序,比如电话号码、身份证号。
4. 实操过程与核心环节实现:手把手跑通一个排序工具
光看不练假把式。这一节我带你从零开始,把排序真正用起来,覆盖几个最典型的场景:C++实现八大排序、Python多维列表排序、Java的List和HashMap排序、MySQL的排序优化、前端表头排序。
4.1 用C++实现八大排序的代码骨架
我先把八大排序的接口统一一下,方便你对照学习:
cpp复制void bubbleSort(int arr[], int n); // 冒泡排序
void selectionSort(int arr[], int n); // 选择排序
void insertionSort(int arr[], int n); // 插入排序
void shellSort(int arr[], int n); // 希尔排序
void mergeSort(int arr[], int l, int r);// 归并排序
void quickSort(int arr[], int l, int r);// 快速排序
void heapSort(int arr[], int n); // 堆排序
void countingSort(int arr[], int n); // 计数排序
我建议你上手的时候,先写一个测试框架,随机生成不同规模的数据,验证排序结果的正确性和耗时。这个习惯特别重要,因为排序代码最容易出现“小数据对,大数据错”的问题,你手动检查根本看不出来。
一个简单的测试思路:
cpp复制#include <iostream>
#include <cstdlib>
#include <ctime>
#include <algorithm>
bool checkSorted(int arr[], int n) {
for (int i = 1; i < n; i++) {
if (arr[i - 1] > arr[i]) return false;
}
return true;
}
然后用标准库的std::sort作为基准答案,对照你自己的排序实现。对了就是对的,错了就回去debug。这种“对照验证”的方法,我强烈推荐每个初学者都掌握。
4.2 Python多维列表按某一个位置的值排序
Python里排序最常用的就是内置函数sorted()和列表的sort()方法。多维列表按某一个位置排序,核心就是key参数。比如有一个学生列表,每个元素是[姓名, 年龄, 分数]:
python复制students = [
["张三", 20, 88],
["李四", 19, 92],
["王五", 21, 85],
["赵六", 20, 91]
]
# 按年龄排序
sorted_by_age = sorted(students, key=lambda x: x[1])
# 按分数降序排序
sorted_by_score = sorted(students, key=lambda x: x[2], reverse=True)
# 先按年龄升序,再按分数降序
sorted_mixed = sorted(students, key=lambda x: (x[1], -x[2]))
这里有个关键点:用负数实现降序,只适用于数值类型。如果是字符串想反向排序,不能取负,得用两次排序或functools.cmp_to_key。我见过有人想按字符串长度降序,写了key=lambda x: -len(x[1]),这是对的,因为len返回整数。但如果想按字符串本身的字典序降序,就必须用reverse=True了。
还有一个技巧:用operator.itemgetter代替lambda,性能更好:
python复制from operator import itemgetter
sorted_by_age = sorted(students, key=itemgetter(1))
在数据量大的时候,itemgetter比lambda快不少,因为它是在C层面实现的。虽然看起来微不足道,但百万级数据排序时,这个差距能到几倍。
4.3 Java中List按某元素排序、HashMap排序
Java里List排序最常用的是Collections.sort()和List.sort()。Java 8之后推荐用Stream和Comparator。给一个实体类排序:
java复制List<User> users = getUsers();
// 按年龄升序
users.sort(Comparator.comparingInt(User::getAge));
// 按年龄降序
users.sort(Comparator.comparingInt(User::getAge).reversed());
// 先按年龄升序,再按姓名降序
users.sort(Comparator.comparingInt(User::getAge)
.thenComparing(Comparator.comparing(User::getName).reversed()));
写Comparator最容易踩的坑,是reversed()的作用范围。很多人以为comparing(...).thenComparing(...).reversed()是把两个比较器都反转,但实际上**reversed()只反转了前面已经组合好的部分**。如果你想两个条件都反转,得写成:
java复制users.sort(Comparator.comparingInt(User::getAge)
.thenComparing(Comparator.comparing(User::getName)).reversed());
// 上面这是整体反转
users.sort(Comparator.comparingInt(User::getAge).reversed()
.thenComparing(Comparator.comparing(User::getName).reversed()));
// 这才是分别反转
HashMap排序是另一个经典场景。HashMap本身是无序的,但你可以把它转成List再排序:
java复制Map<String, Integer> scoreMap = new HashMap<>();
// 填充数据...
// 按value升序排序
List<Map.Entry<String, Integer>> list = new ArrayList<>(scoreMap.entrySet());
list.sort(Map.Entry.comparingByValue());
// 按value降序
list.sort(Map.Entry.comparingByValue(Comparator.reverseOrder()));
注意,排序的结果是List,不是Map。如果你想要一个有序的Map,应该用LinkedHashMap来接收,这样遍历顺序就是插入顺序,也就是排好序的顺序。
4.4 MySQL排序与索引的关系、前端表头排序
数据库排序是个大话题,这里只讲最核心的一点:ORDER BY能不能用索引,决定了查询是秒回还是几秒。
如果你执行SELECT * FROM orders ORDER BY create_time DESC,而create_time上有索引,MySQL会直接按索引倒序扫描,不需要额外的排序操作,这个叫Using index。如果没有索引,MySQL会把查询结果放进临时表,用文件排序filesort,数据量大时性能会急剧下降。
判断方法很简单,用EXPLAIN看执行计划:
sql复制EXPLAIN SELECT * FROM orders ORDER BY create_time DESC;
如果Extra列显示Using filesort,就说明排序没有走索引,需要优化。优化方式一般有三种:加索引、减少排序字段、尽量让查询条件命中索引。
前端表头排序则简单得多,本质就是JavaScript数组的sort方法。但要注意两件事:一是默认的sort比较的是字符串,不是数值,所以必须传比较函数;二是如果排序的数据是表格行DOM,直接用Array.prototype.sort配合appendChild就可以实现重排,不需要重新渲染整个表格。
javascript复制const tbody = document.getElementById('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
function sortTable(colIndex, isNumeric) {
rows.sort((a, b) => {
const aVal = a.cells[colIndex].textContent.trim();
const bVal = b.cells[colIndex].textContent.trim();
if (isNumeric) return parseFloat(aVal) - parseFloat(bVal);
return aVal.localeCompare(bVal, 'zh-Hans-CN');
});
rows.forEach(row => tbody.appendChild(row));
}
这里我给你提个醒:字符串比较优先用localeCompare,不要直接用a > b。特别是中文排序,如果用默认的Unicode码点排序,结果跟你在字典里看到的不一样。localeCompare能按中文习惯排序,而且支持指定语言区域,实用性很强。
4.5 推荐一个学习路线
如果你现在刚开始学排序,我建议按这个顺序走:
- 先手写冒泡、选择、插入这三种O(n²)的排序,理解“比较+交换”的基本模型。
- 再学希尔排序,理解“分组+插入”的优化思想。
- 然后学归并和快排,理解“分治+递归”的高级模型。
- 最后学堆排序,理解“堆”这个数据结构的应用。
- 有余力再看计数、基数、桶排序,理解“非比较排序”的不同思路。
每一步都要写测试代码验证正确性,不要看完就当学会了。排序这种东西,自己动手写过一遍,和看十遍完全是两个概念。
5. 常见问题与排查技巧实录
下面这些问题是真实开发中经常遇到的,我整理成一个速查表,你遇到的时候直接对照排查。
5.1 排序结果不对的四个高频原因
| 症状 | 可能原因 | 排查方向 |
|---|---|---|
| 小数在前大数在后,但顺序乱 | 比较函数写反了 | 检查返回值:负数表示a排在b前面 |
| 字符串排序结果和字典序不符 | 默认按Unicode码点排序 | 检查是否用了localeCompare |
| 相同数值的元素顺序被打乱 | 使用的排序算法不稳定 | 换成归并排序等稳定算法 |
| 大数据量时结果错误 | 递归深度溢出或边界条件错误 | 检查数组越界,用小数据加打印日志定位 |
5.2 百万级数据排序变卡的排查与优化
我遇到过一个真实案例,一个排行榜接口,数据量大约几十万,每次请求都要全量排序,响应时间越来越慢。排查逻辑是这样的:
第一步,先用EXPLAIN看SQL执行计划,发现ORDER BY没有走索引,触发了Using filesort。
第二步,加索引后,发现效果不明显,因为查询条件里的WHERE过滤掉了大量数据,索引排序的好处没发挥出来。
第三步,进一步分析发现,接口返回的数据其实只需要前100名,根本不需要全量排序。于是改成用堆排序的思路,维护一个大小为100的最小堆,遍历一遍数据就得到前100名,时间从几秒降到了几十毫秒。
这个案例告诉我们一个核心原则:排序前先想清楚,你需要的到底是“全排序”还是“Top N”。只取前N名的话,用堆排序或者SQL的LIMIT配合索引,效率高得多。
5.3 排序统计与字符串排序的注意事项
字符串排序比数字排序复杂,因为涉及字符集和排序规则。比如JavaScript里:
javascript复制// 默认排序,按Unicode码点
['apple', 'Banana', 'cherry'].sort();
// 结果:['Banana', 'apple', 'cherry'],因为大写字母码点在小写字母前面
// 忽略大小写排序
['apple', 'Banana', 'cherry'].sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
// 结果:['apple', 'Banana', 'cherry']
另外,中文排序更麻烦。localeCompare在不同浏览器和Node版本的行为可能不一致,如果是服务端排序,建议统一用ICU(International Components for Unicode)或者自定义的拼音排序规则。
5.4 拓扑排序中的环检测
拓扑排序是图论里的概念,用于有向无环图(DAG)的节点排序,常用于任务调度、课程安排、依赖解析。它的核心逻辑不算复杂,但有一个必须处理的情况:如果图里有环,拓扑排序无法完成。
实现拓扑排序有两种方式:Kahn算法(基于入度)和DFS(基于深度优先搜索)。Kahn算法做环检测尤其方便:每次从队列里取出一个入度为0的节点,把它从图中移除,更新邻居的入度;如果最后处理完的节点数小于总节点数,就说明图里有环。
C语言实现拓扑排序的关键代码如下:
c复制// graph用邻接表表示,indegree数组记录每个节点的入度
void topologicalSort(int n, int indegree[], int adj[][MAXN], int result[]) {
int queue[MAXN], head = 0, tail = 0, count = 0;
for (int i = 0; i < n; i++) {
if (indegree[i] == 0) {
queue[tail++] = i;
}
}
while (head < tail) {
int u = queue[head++];
result[count++] = u;
for (int v = 0; v < n; v++) {
if (adj[u][v]) {
indegree[v]--;
if (indegree[v] == 0) {
queue[tail++] = v;
}
}
}
}
if (count != n) {
// 存在环,排序失败
}
}
如果遇到环,我的建议是:不要直接报错,而是把环中的节点单独标记出来,方便排查依赖关系。很多构建工具就是这么做的。
5.5 五个排查排序问题的实用技巧
第一个技巧,写一个能复现的最小用例。不要拿生产数据排错,先把问题缩小到一个能稳定复现的小数组或小列表上,然后逐步调试。
第二个技巧,打印中间状态。比如快排的时候,把分区后的数组打出来,看看基准值的位置对不对。这一个简单方法能解决80%的排序bug。
第三个技巧,用标准库排序作为对照。不确定自己的排序有没有问题时,用std::sort、Arrays.sort()、Python的sorted()跑一遍同样的数据,对比结果。如果自己的结果跟标准库不一致,第一反应不应该是觉得标准库错了,而是检查自己的代码。
第四个技巧,注意是否涉及数据类型的隐式转换。JavaScript里尤其明显,[10, 9, 100].sort()默认返回[10, 100, 9],因为默认按字符串比较。必须传(a, b) => a - b。
第五个技巧,排序前先备份一份原始数据。有时候排序逻辑有bug会直接改动原数组,导致后续调试拿不到原始数据。养成先复制再排序的习惯,能省不少事。
5.6 用排序思想解决“看似与排序无关”的问题
最后我想分享一个我觉得排序思想最值钱的地方:很多看起来和排序没关系的问题,本质上都是排序问题。
比如说,你有一批定时任务,每个任务有优先级和执行时长,怎么调度才能让平均等待时间最短?答案是按最短执行时间优先排序。
再比如,你有一个商品列表,每个商品有多个评价维度,怎么综合排序才能让用户快速找到想要的商品?这本质上是多关键字排序问题。
又比如,一群人的身高各不相同,要排成两排,要求前排的人比后排对应位置的人矮。这个问题的本质也是排序加分组。
我的意思是,学排序不只是学那十几段代码,更是学那种“建立次序、权衡优先级、在约束条件下找最优排列”的思维方式。这种思维一旦建立,你看很多项目问题都会有一种“这可以用排序解决”的直觉,这是刷多少道题都换不来的。
根据我个人经验,排序是那种“看十遍不如写一遍”的东西。我到现在还记得当年把快排写错,对着屏幕debug了两个小时的场景,但那次踩坑让我真正理解了分区和基准值的意义。希望你读到这里之后,不要急着往下看下一篇,先找一道排序题目,亲手写一遍代码,跑一遍测试,感受一下那些边界条件和性能差异。等你能不看参考代码写出一个稳定的归并排序时,你就真正迈过“初识基本排序”这道门槛了。
