1. 排序算法到底在解决什么问题:先看一个具体例子
你手里有没有遇到过这种数据:一个列表,里面存着用户的注册时间、商品的销量、或者一场考试的所有成绩,但顺序是乱的。比如 [64, 34, 25, 12, 22, 11, 90] 这样一组数,你想找出第二名是谁、想按从高到低展示、想统计前三个最大值,不排序根本没法做——当然你可以反复遍历去“找”,但数据量一旦上千、上万,这种暴力做法就会慢到你怀疑人生。
排序算法的意义,就是给这种“乱序查找”提供一条标准化的路:一次排序,反复受益。排序完之后,二分查找、Top K、去重、合并、求中位数这些操作都有了高效的土壤。这也是为什么数据结构与算法里,排序永远是第一个被系统讲解的模块,也是各类笔试面试的高频考点。
本文就用“举例”的方式,把冒泡、选择、插入、希尔、快排、归并、堆排、计数、桶、基数这十大排序算法逐个拆开,配上可运行的 Python 代码、每一趟的结果演示、复杂度的推算思路,以及最重要的——它们各自适合什么场景,不适合什么场景。不管你是正在准备面试的应届生,还是工作中想优化一把数据处理的工程师,这篇文章都能直接拿去对照着用。
在进入具体算法之前,你还需要建立一把“尺子”,否则后面每个算法看完只觉得“都挺厉害”但不知道差别在哪。排序算法的评估维度一共就四个:
- 时间复杂度:数据规模 n 变化时,算法执行时间的增长趋势。常见的有 O(n^2)、O(n log n)、O(n+k) 这些。
- 空间复杂度:排序过程中额外占用的内存,原地排序是 O(1),归并这种就是 O(n)。
- 稳定性:相同的两个元素,排序后它们的相对顺序是否保持不变。稳定排序在“先按时间排,再按优先级排”这种多关键字排序里非常重要。
- 原地性:是否只需要常数级别的额外空间,这决定了你能否在内存受限的环境里完成排序。
为了后文方便对照,我把十大排序的这四项指标先列出来:
| 排序算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|
| 冒泡排序 | O(n^2) | O(n^2) | O(1) | 稳定 |
| 选择排序 | O(n^2) | O(n^2) | O(1) | 不稳定 |
| 插入排序 | O(n^2) | O(n^2) | O(1) | 稳定 |
| 希尔排序 | O(n^1.3) | O(n^2) | O(1) | 不稳定 |
| 快速排序 | O(n log n) | O(n^2) | 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 + k) | O(n^2) | O(n + k) | 稳定 |
| 基数排序 | O(d * (n + k)) | O(d * (n + k)) | O(n + k) | 稳定 |
这张表先放在这里,后面的每个例子都会对号入座做解释。你现在不需要背下来,看完算法实现和推导过程,这张表会自然地印在脑子里。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 三个最基础的排序:冒泡、选择、插入的实现与对比
最经典的入门三件套,虽然后面你会发现它们在真实项目中很少被直接使用,但它们是理解更高级算法的基础——因为所有高级排序,本质上都是在想办法减少这三个算法的比较和交换次数。
2.1 冒泡排序:每一趟把最大值“浮”到最后
冒泡的思想非常直观:从左到右依次比较相邻的两个元素,如果左边比右边大,就交换位置。这样每一趟结束后,当前范围内的最大值就像气泡一样浮到最右边。下一趟只需要处理前面未排序的部分。
python复制def bubble_sort(arr):
n = len(arr)
for i in range(n):
# 每趟结束后,最后 i 个元素已经就位,无需再比较
swapped = False
for j in range(0, n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
swapped = True
# 如果这一趟没有发生任何交换,说明已经有序,提前退出
if not swapped:
break
return arr
注意这里我加了一个 swapped 标志位——这是冒泡排序一个很实用的优化。如果某一趟遍历中一次交换都没发生,说明数组已经全部有序,后面的趟数都是浪费。用 [1, 2, 3, 4, 5] 这种已经有序的输入测试时,加入标志位后,最优时间复杂度直接从 O(n^2) 降到了 O(n)。
冒泡排序的推导也最简单:外层循环跑 n 趟,内层每趟最多做 n 次比较,所以总比较次数约等于 n * (n-1) / 2,时间复杂度 O(n^2)。因为交换只在相邻元素之间进行,且只有 arr[j] > arr[j+1] 时才交换,值相等的元素不会被交换位置,所以它是稳定的。
2.2 选择排序:每一趟找出最小值放到最前面
选择排序的思路和冒泡正好反过来——它不急着交换,而是先扫描整个未排序区间,找到最小值的下标,然后一次性把它放到区间的最前面。
python复制def selection_sort(arr):
n = len(arr)
for i in range(n):
min_idx = i
for j in range(i + 1, n):
if arr[j] < arr[min_idx]:
min_idx = j
arr[i], arr[min_idx] = arr[min_idx], arr[i]
return arr
选择排序没有冒泡那么多无谓的交换,每趟最多只交换一次。但它的比较次数依然是 n^2 / 2 这个量级,所以时间复杂度同样是 O(n^2)。空间上它是原地排序 O(1)。
不过有一个特别容易被忽略、面试也常被追问的点:选择排序是不稳定排序。原因是“远距离交换”可能破坏相同元素的相对顺序。举个最直白的例子:[5a, 8, 5b, 1],第一趟找到最小值 1,和下标 0 的 5a 交换,数组变成 [1, 8, 5b, 5a]——原来 5a 在 5b 前面,现在 5b 跑到 5a 前面去了。如果你在维护一条“按时间排序后再按金额排序”的记录表,这种不稳定会直接导致第二次排序的结果不符合预期。
2.3 插入排序:像整理扑克牌一样逐个插入
插入排序的思路你在打扑克牌时肯定用过——摸到一张新牌,从右往左找到它应该在的位置,然后插进去。计算机实现起来,就是在已排序区间从右往左扫描,找到合适位置后,把后面的元素整体右移一位。
python复制def insertion_sort(arr):
n = len(arr)
for i in range(1, n):
key = arr[i]
j = i - 1
# 从右往左找插入位置,同时把大的元素右移
while j >= 0 and arr[j] > key:
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = key
return arr
插入排序值得一提的地方在于:它对“近似有序”的数据非常友好。如果输入数据原本就基本有序,插入排序的内部 while 循环几乎不怎么执行,时间复杂度可以逼近 O(n)。这个特性在后面的高级排序里会被大量利用。
比如对 [2, 3, 4, 5, 1] 这个序列,前面的 2、3、4、5 本来就有序,插入排序只需要做最后一次插入操作,把 1 移动四位即可,总共才比较 4 次。这比冒泡和选择都快得多。
三个算法虽然都是 O(n^2) 级别,但常数因子差别很大。实测一个 5000 个随机数的数组,插入排序通常比冒泡快 2 到 3 倍,比选择排序快 1.5 倍左右。这也是为什么 MDN 上 JavaScript 引擎的 Array.prototype.sort 在对小规模数组排序时,底层会选择插入排序——因为对小数组来说,插入排序的“简洁”本身就是一种性能优势。
3. 从 O(n^2) 到 O(n log n):快排、归并、堆排的核心机制
如果你刷过算法题,一定感受过 O(n^2) 在数据规模大了之后的绝望。当 n 从 1000 变成 100 万,n^2 的量级就是 10^12 次操作,而 n log n 大概是 2 * 10^7,差了整整 5 个数量级。下面这三个算法,就是把排序从“平方级”拉进“线性对数级”的核心代表。
3.1 快速排序:选一个基准,分而治之
快排在所有排序算法中名声最响,不是因为它的性能永远最好,而是因为它的常数因子小、缓存友好、实现灵活。它的核心思想是分治:从数组里选一个基准值 pivot,把所有小于 pivot 的放到左边,大于 pivot 的放到右边,然后递归地对左右两个子区间做同样的事情。
python复制def quick_sort(arr):
if len(arr) <= 1:
return arr
pivot = arr[len(arr) // 2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quick_sort(left) + middle + quick_sort(right)
上面这个写法是“教学版”,逻辑清晰但额外使用了大量空间。工程上更常用的是原地分区版本——通过双指针交换,把小于 pivot 的元素逐步挪到左侧:
python复制def quick_sort_inplace(arr, low, high):
if low >= high:
return
pivot = arr[(low + high) // 2]
i, j = low, high
while i <= j:
while arr[i] < pivot:
i += 1
while arr[j] > pivot:
j -= 1
if i <= j:
arr[i], arr[j] = arr[j], arr[i]
i += 1
j -= 1
quick_sort_inplace(arr, low, j)
quick_sort_inplace(arr, i, high)
这里有一个很多初学者容易踩的坑:递归调用的区间边界不是 low, pivot_index - 1 和 pivot_index + 1,而是 low, j 和 i, high。因为经过多次交换后,pivot 不一定待在最初的中间位置,i 和 j 才是左右两个分区的真实分界点。我见过不少人在写原地快排时就卡在这一行。
快排的平均时间复杂度是 O(n log n),但最坏情况下——比如每次选的 pivot 恰好是当前区间里的最小或最大值——时间复杂度会退化成 O(n^2)。解决办法是“三数取中”或者随机选 pivot,让最坏情况出现的概率降到几乎为零。真实世界里,STL 的 introspective sort(内省排序)就是监测到递归深度超过阈值时自动切到堆排序,防止快排退化,这个设计思路很值得借鉴。
3.2 归并排序:先拆分到最小,再有序地合并
归并排序的思路是“先把问题拆小,再逐层有序合并”。它把数组从中间一分为二,递归地对左右两半排序,然后通过一个双指针合并过程把两个有序的半区合成一个有序的整体。
python复制def merge_sort(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
return merge(left, right)
def merge(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
归并排序最大的优点是稳定,而且时间复杂度无论什么情况都是 O(n log n),没有快排那种“输入数据分布决定命运”的问题。它的代价是需要额外 O(n) 的空间来存放合并结果。数组越长,内存开销越可观。对 1000 万个整数的数组做归并,光辅助数组就要约 80MB 内存,这在内存敏感的嵌入式环境里可能就直接不可用了。
但归并排序不仅仅是教科书产物。在真实世界里,Java 的 Arrays.sort() 对对象数组使用稳定排序时选择的就是归并思想的变种 TimSort;Python 内置的 list.sort() 同样也是 TimSort。为什么对象排序一定要稳定?因为真实业务里你几乎总是面对“多关键字”排序需求——先按部门排、再按入职时间排,稳定排序可以在不破坏第一次排序结果的前提下完成第二次排序。
归并排序的另一个用处是解决一类经典算法题:逆序对统计。你在归并的合并过程中,每当从右侧数组取一个元素放入结果时,左侧数组中尚未放入的所有元素都大于当前元素,这些就是“逆序对”。一个简单的归并排序代码,稍加改动就能统计逆序对数量,这在实际工作中可以用来衡量两个排序结果的相似度。
3.3 堆排序:利用二叉堆完成选择排序的升级版
堆排序的思路可以理解为“带索引的选择排序”——选择排序每一趟都要线性扫描找最小值,而堆排序用二叉堆把“找最小值”的复杂度降到了 O(log n)。
python复制def heapify(arr, n, i):
largest = i
left = 2 * i + 1
right = 2 * i + 2
if left < n and arr[left] > arr[largest]:
largest = left
if right < n and arr[right] > arr[largest]:
largest = right
if largest != i:
arr[i], arr[largest] = arr[largest], arr[i]
heapify(arr, n, largest)
def heap_sort(arr):
n = len(arr)
# 建堆
for i in range(n // 2 - 1, -1, -1):
heapify(arr, n, i)
# 逐个将堆顶最大值移到末尾
for i in range(n - 1, 0, -1):
arr[i], arr[0] = arr[0], arr[i]
heapify(arr, i, 0)
return arr
堆排序有很好的硬件优势:它只需要 O(1) 的额外空间,不像归并那样吃内存,也不像快排那样在最坏情况下会退化。所以外部排序、嵌入式环境、实时操作系统里,堆排序经常被选中。
但堆排序有一个很少被提及的问题:它不稳定,而且常数因子偏大。实际执行时,堆排序即使输入已经有序,依然要做完整的建堆和调整流程,无法像插入排序那样提前退出。所以同样 O(n log n) 的三个算法里,纯靠实测速度,快排通常是最快的,堆排序往往垫底。这告诉我们一个经验:复杂度相同不代表性能相同,常数因子和访问模式(顺序访问 vs 跳跃访问)在真实机器上影响巨大。堆排序的数组访问是跳跃式的,对 CPU 缓存很不友好;快排和归并的访问模式更线性,缓存命中率高得多。
3.4 希尔排序:插入排序的“大步快走”改良
希尔排序是 O(n^2) 到 O(n log n) 之间的过渡产物,也是第一个突破 O(n^2) 的排序算法。它把数据按下标的一定增量分组,对每组使用插入排序,然后逐步缩小增量,直到增量为 1。
python复制def shell_sort(arr):
n = len(arr)
gap = n // 2
while gap > 0:
for i in range(gap, n):
temp = arr[i]
j = i
while j >= gap and arr[j - gap] > temp:
arr[j] = arr[j - gap]
j -= gap
arr[j] = temp
gap //= 2
return arr
希尔排序的关键在于“预排序”——大步长时,元素可以快速移动到大致正确的位置,最后一步增量为 1 的插入排序时,数组已经基本有序,插入排序的高效性就能被充分利用。希尔排序的平均时间复杂度取决于步长序列的选择,约为 O(n^1.3),最坏情况下 O(n^2)。
这个算法的价值更多体现在教学意义上:它告诉我们排序算法的优化可能来自“利用数据已有的有序性”,这一观念后来被 TimSort 等现代混合排序发扬光大。
4. 不比较也能排序:计数、桶、基数三种线性复杂度方法
前面说的所有算法都是“基于比较”的排序——通过两两比较来决定元素的先后顺序。这类算法有一个理论天花板:任何基于比较的排序,平均时间复杂度都不可能低于 O(n log n)。证明思路是用决策树模型,n 个元素的排列有 n! 种可能,决策树的高度至少是 log(n!),近似为 n log n。
但世界上存在不需要比较大小的排序方式,它们通过“利用数据本身的分布特征”突破 O(n log n) 的下界,达到 O(n + k) 甚至 O(d * (n + k))。不过这些方法条件苛刻,只在特定场景下有效。
4.1 计数排序:适合取值范围有限的整数数据
计数排序的思想非常朴素:既然你要排序的是 0 到 k 之间的整数,那我直接数一下每个数出现了多少次,然后按从前往后的顺序把数一一输出即可。
python复制def counting_sort(arr):
if not arr:
return arr
max_val = max(arr)
count = [0] * (max_val + 1)
for num in arr:
count[num] += 1
result = []
for i in range(len(count)):
result.extend([i] * count[i])
return result
这个版本很好理解,但它不是稳定排序——因为它在输出时完全不关注相同元素的相对顺序。要写出稳定的计数排序,需要把“计数”改造成“前缀和”,然后从后往前填充结果数组:
python复制def counting_sort_stable(arr):
max_val = max(arr)
count = [0] * (max_val + 1)
for num in arr:
count[num] += 1
# 前缀和
for i in range(1, max_val + 1):
count[i] += count[i - 1]
output = [0] * len(arr)
for num in reversed(arr):
output[count[num] - 1] = num
count[num] -= 1
return output
为什么从后往前遍历?因为 count[num] - 1 是当前这个值应该放到的最后一个位置,倒序遍历可以保证相同值的元素保持原来的顺序。计数排序的时间复杂度 O(n + k),空间复杂度 O(k),k 是最大数值范围。但它的硬伤也在这——如果最大最小值之间差距巨大(比如 [1, 999999999]),k 就会大到不可接受。
实际业务里,计数排序最常见的应用是对考试成绩、年龄、星级评分这类分布集中的整数进行排序。有朋友在游戏公司做排行榜,日活上百万用户,玩家分数范围固定 0 到 10000 分,他用计数排序做全量排序,内存占用可控,速度比快排还快几倍——这就是数据范围优势。
4.2 桶排序:把数据先分到多个桶里再各自排序
桶排序可以看成是计数排序的推广。计数排序的每个“桶”只能装一个值,而桶排序的每个桶可以装一个区间的多个元素,桶内再用任何排序算法(通常是插入排序)来排。
python复制def bucket_sort(arr, bucket_size=5):
if not arr:
return arr
min_val, max_val = min(arr), max(arr)
bucket_count = (max_val - min_val) // bucket_size + 1
buckets = [[] for _ in range(bucket_count)]
for num in arr:
idx = (num - min_val) // bucket_size
buckets[idx].append(num)
result = []
for bucket in buckets:
insertion_sort(bucket)
result.extend(bucket)
return result
桶排序的性能取决于桶的数量和数据的分布。如果数据均匀分布在区间内,每个桶的元素数都差不多,总复杂度约为 O(n + k)。但如果数据极度倾斜,比如所有数据都落在一个桶里,桶排序就会退化为那个桶内排序算法的时间复杂度——如果是插入排序,就是 O(n^2)。
使用桶排序的经典场景看起来似乎有点过时——对浮点数排序。因为比较排序处理浮点数也完全没问题,这里主要体现桶的思想:比如对 0 到 1 之间的随机浮点数排序,你可以分成 10 个桶 [0, 0.1)、[0.1, 0.2) 等等,每个桶里数据量大致均匀,桶内用插入排序效率极高。
4.3 基数排序:按位数逐轮排序的数字拆分术
基数排序的思路是按位排序,从最低位开始,依次对元素的每一位进行稳定排序。比如对三位数排序,先按个位排,再按十位排,最后按百位排。只要每一轮使用的都是稳定排序,最终结果就一定是有序的。
python复制def radix_sort(arr):
max_val = max(arr)
exp = 1
while max_val // exp > 0:
counting_sort_by_digit(arr, exp)
exp *= 10
return arr
def counting_sort_by_digit(arr, exp):
n = len(arr)
output = [0] * n
count = [0] * 10
for num in arr:
digit = (num // exp) % 10
count[digit] += 1
for i in range(1, 10):
count[i] += count[i - 1]
for num in reversed(arr):
digit = (num // exp) % 10
output[count[digit] - 1] = num
count[digit] -= 1
return output
以 [170, 45, 75, 90, 2, 802, 24, 66] 为例,第一轮按个位数排序后变成 [170, 90, 2, 802, 24, 45, 75, 66],第二轮按十位排后变成 [2, 802, 24, 45, 66, 170, 75, 90],第三轮按百位排后就是 [2, 24, 45, 66, 75, 90, 170, 802]。每一位上的排序都利用了计数排序,总复杂度是 O(d * (n + k)),其中 d 是最大数字的位数。
基数排序适合那种“位数不多、数据量极大”的排序场景——比如排序 1000 万个 18 位身份证号的某几位,或者手机号码排序。这类数据用快排需要 O(n log n) 次比较,而基数排序只需要若干轮线性扫面,实测往往更快。但它的局限性也很明显:只能处理非负整数或可以映射成非负整数的数据(比如日期转成 YYYYMMDD 数字),对字符串排序时需要额外处理字符集映射,复杂度会显著上升。
5. 实战选型:怎么从十大排序里挑出最合适的那一个
看完这么多算法,真正的考验来了——工作中你面前摆着一份数据,到底选哪种排序?我的建议很简单:个人项目里直接用库函数,别自己造轮子;面试或框架代码里,按下面几个维度做决策。
5.1 第一条铁律:默认用内置排序
Python 的 sorted()、Java 的 Arrays.sort()、C++ 的 std::sort(),这些标准库排序经过了十几年的优化迭代,早已不是教科书的简单实现。CPython 的 TimSort 融合了归并排序和插入排序,并且专门针对真实数据中常见的“部分有序”模式做了优化;JDK 8+ 对数组排序时,小数组用插入排序、大数组用双轴快排或 TimSort。你自己从零实现一个排序,性能大概率不如标准库,除非你的数据形态极其特殊。
但理解这些算法绝不是“没用”的——因为你在写代码时经常需要判断“该不该依赖排序”“排序能不能被替代”“如果排序 O(n log n) 成为瓶颈,是否有线性方法”。这些问题要求你对排序原理有透彻理解。
5.2 四个关键问题的决策树
当你确实需要手写排序时,问自己四个问题:
数据规模有多大? 如果 n 小于 50,插入排序几乎总是最优选择。它的常数因子极小,没有递归调用,对小数组甚至比快排更快。JDK 源码里 INSERTION_SORT_THRESHOLD 设置为 47,就是这个道理。
是否对稳定性有要求? 如果后续还要按另一个关键字排序、或者需要保持原始顺序的某些信息,必须选稳定排序。这时候归并排序、Timsort 就是首选,快排再快也不能用。
内存够不够? 归并排序需要 O(n) 的辅助空间,如果你处理的数组是几十 GB 的日志文件,内存根本无法容纳,这时候要么选快排(递归栈开销小),要么选堆排序(严格的 O(1) 空间)。外部排序场景下甚至会用堆排序做多路归并。
数据本身有什么特征? 如果数据是范围有限的非负整数,计数排序是降维打击;如果是大量固定位数的整数,基数排序可能比快排快得多;如果数据近乎有序,插入排序和 TimSort 胜出。这些“非常规”场景,恰恰是非比较排序算法的用武之地。
用一张表格快速总结:
| 场景特征 | 推荐排序 | 原因 |
|---|---|---|
| 小规模数据(n < 50) | 插入排序 | 常数因子小,实现简单 |
| 常规大规模数据,内存充足 | 快排 | 时间 O(n log n),常数因子小,缓存友好 |
| 需要稳定排序 | 归并排序 / TimSort | 保持相同元素相对顺序 |
| 内存严格受限 | 堆排序 | 原地排序 O(1) 空间 |
| 整数数组,取值范围小 | 计数排序 | 时间复杂度 O(n + k) |
| 浮点数,分布均匀 | 桶排序 | 线性期望时间 |
| 定长整数/日期/ID | 基数排序 | 多轮线性扫描,避开比较 |
5.3 我实际工作中最常踩的排序“坑”
最后分享几个我在真实项目里踩过的坑,帮你提前避开。
坑一:用快排处理已经有序的大数组。 如果你实现的快排每次取第一个元素作为 pivot,那么对一个已经排好序的 100 万整数做排序,递归深度会达到 100 万层,直接栈溢出。解决办法很粗暴——pivot 取中间值或者随机值。这在性能测试里很难复现,但在生产环境如果用户上传的数据恰好是排过序的,就一定会触发。
坑二:忽略了 Python 的 sorted() 返回新列表。 Python 的 list.sort() 是原地修改,sorted() 返回新列表。很多人用 sorted(arr) 想修改 arr,结果发现原数组根本没变。这不算排序算法本身的坑,但在处理大数组时,额外复制一份列表会带来明显的内存和耗时开销。
坑三:对相同元素很多的数组,快排的性能反而会崩。 经典快排在处理 [1] * 1000000 这种全等数组时会退化到 O(n^2)——因为所有元素都等于 pivot,分区后左区间为空右区间为 n-1,递归深度爆炸。解决方案是采用“三向切分”(Dutch National Flag 思想),把数组分成小于、等于、大于三部分。这也是为什么 Python 内置的 TimSort 在应对这种输入时有优势——它的归并逻辑天然对重复元素友好。
坑四:在“只需要 Top K”的问题里做了全量排序。 如果你只需要找出前 100 个最大的数,完全没必要把 1000 万个数据全部排序。用堆排序做“局部排序”,维护一个大小为 100 的最小堆,遍历一遍数据,复杂度只有 O(n log K) 而不是 O(n log n)。当 K 远小于 n 时,这个差距是巨大的。
排序算法是那种“看着简单、真正吃透不容易”的知识点。你会写冒泡排序不代表你懂排序,能解释清楚为什么归并排序稳定、为什么快排必须随机选 pivot、为什么计数排序对浮点数无效,才说明你是真的理解了排序这件事。在面试和实战中,算法永远是为数据和场景服务的——把每个算法的适用边界和复杂度代价刻在脑子里,比背下十套实现代码有用得多。
