Skip to content

排序算法对比

场景引入

学完各种排序算法后,最常见的困惑是:这么多排序算法,到底该用哪个?

本文将所有主流排序算法放在一起做全面对比,帮你建立系统化的认识。

十大排序算法总览

比较类排序

比较类排序通过元素之间的比较来决定顺序,理论下界为 O(n log n)。

算法最好时间平均时间最坏时间空间稳定性原地
冒泡排序O(n)O(n²)O(n²)O(1)稳定
选择排序O(n²)O(n²)O(n²)O(1)不稳定
插入排序O(n)O(n²)O(n²)O(1)稳定
希尔排序O(n log n)O(n^1.3)*O(n²)O(1)不稳定
归并排序O(n log n)O(n log n)O(n log n)O(n)稳定
快速排序O(n log n)O(n log n)O(n²)O(log n)不稳定
堆排序O(n log n)O(n log n)O(n log n)O(1)不稳定

*希尔排序的复杂度取决于增量序列的选择,O(n^1.3) 是 Knuth 增量序列的经验值。

非比较类排序

非比较类排序利用数据的特殊性质,可以突破 O(n log n) 的下界。

算法最好时间平均时间最坏时间空间稳定性适用条件
计数排序O(n + k)O(n + k)O(n + k)O(k)稳定整数,值域 k 不大
基数排序O(n × d)O(n × d)O(n × d)O(n + k)稳定整数/字符串,d 为位数
桶排序O(n + k)O(n + k)O(n²)O(n + k)稳定数据均匀分布

排序算法选择决策图

如何选择排序算法?

按数据规模

数据规模推荐算法原因
n < 50插入排序常数因子小,代码简单
50 < n < 1000快速排序/归并排序O(n log n) 开始体现优势
n > 1000快速排序(通用)/ 归并排序(需稳定)性能最优
n 极大,内存不足外部归并排序唯一支持外部排序的方案

按数据特征

数据特征推荐算法原因
基本有序插入排序最好 O(n),移动次数少
完全随机快速排序平均性能最优
大量重复元素三路快排相等元素不再参与递归
需要稳定排序归并排序唯一稳定的 O(n log n) 比较排序
值域小的整数计数排序O(n + k),突破比较排序下界
需要原地 + 最坏 O(n log n)堆排序唯一满足这两个条件的算法
链表排序归并排序不需要随机访问,空间可优化为 O(1)

按需求优先级

追求平均性能 → 快速排序(随机化基准)

追求最坏保证 → 堆排序或归并排序

追求稳定性 → 归并排序

追求空间最省 → 堆排序(O(1) 额外空间)

数据流式到达 → 插入排序(在线算法)

JavaScript 的 Array.sort() 用什么算法?

JavaScript 的 Array.sort() 在不同引擎中实现不同:

V8 引擎(Chrome / Node.js)

V8 在 2019 年将排序算法从快速排序切换为 TimSort。TimSort 是一种混合排序算法,由 Tim Peters 于 2002 年为 Python 设计,核心思路是:

  1. 发现自然有序片段(Run):扫描数组,找到已经有序的连续子序列
  2. 短 Run 用插入排序扩展:如果 Run 太短(< minrun,通常 32-64),用插入排序补齐
  3. 合并 Run:用改进的归并排序将各个 Run 合并,合并策略确保栈上 Run 的长度满足特定不变式

TimSort 的优势

  • 最好情况 O(n):已有序数据只需一次扫描
  • 最坏情况 O(n log n):不会退化
  • 稳定排序:保持相等元素的相对顺序
  • 对现实数据高效:现实中的数据往往具有局部有序性,TimSort 能利用这一特点

SpiderMonkey(Firefox)

同样使用归并排序的变种。

重要提示

Array.sort() 默认按字符串字典序排序!排数字必须传比较函数:

javascript
// 错误:[1, 10, 2, 20, 3] → [1, 10, 2, 20, 3](字典序)
[3, 1, 20, 10, 2].sort()

// 正确:[1, 2, 3, 10, 20]
[3, 1, 20, 10, 2].sort((a, b) => a - b)

各算法的核心价值

不同排序算法的学习侧重点不同:

核心算法

  1. 快速排序 — partition 思想应用极广,分区、快速选择都基于它
  2. 归并排序 — 分治思想的标杆,merge 操作衍生出逆序对、链表排序等经典问题
  3. 堆排序 — 重点在堆这个数据结构本身,Top K、优先队列都离不开它

实用算法

  1. 插入排序 — TimSort 的核心组件,小数组和近乎有序数据的最优选择
  2. 计数排序 — 非比较排序的代表,值域小时效率极高
  3. 桶排序 — 理解分桶思想,很多工程问题的优化都用到类似策略

基础算法

  1. 冒泡排序 — 最直观的排序,适合作为理解其他算法的起点
  2. 选择排序 — 理解不稳定性的最佳案例
  3. 希尔排序 — 增量序列的概念,突破 O(n²) 的一种思路
  4. 基数排序 — 按位排序,字符串和大整数场景下的利器

实测对比:性能增长曲线

理论复杂度只是渐进分析,实际性能还取决于常数因子。下面的工具在你的浏览器中实时运行七种排序算法,从 n=50 到 n=10000 绘制性能增长曲线:

加载可视化中...

几个值得关注的现象

切换不同数据分布试试:

  • 随机数据:O(n²) 算法和 O(n log n) 算法的差距一目了然——前者的曲线呈抛物线增长,后者几乎是平的
  • 已排序数据:插入排序 O(n) 碾压所有算法;冒泡排序也是 O(n);但注意观察希尔排序的表现
  • 逆序数据:冒泡排序和插入排序都是最坏情况 O(n²),但插入排序的移动次数约为冒泡交换次数的同一量级(每次移动只需 1 次赋值 vs 交换的 3 次赋值)
  • 基本有序:插入排序接近 O(n),快速排序仍然保持 O(n log n)

切换纵轴指标试试:

  • 「耗时」看实际运行速度,受 JS 引擎优化影响
  • 「比较次数」看算法本质的工作量
  • 「交换/移动次数」看数据搬运的开销

为什么插入排序比冒泡排序快?

从实测数据可以看到,两者都是 O(n²),但插入排序的实际操作量更少:

冒泡排序插入排序
每次交换的赋值次数3 次(temp = a; a = b; b = temp)1 次(a[j+1] = a[j])
平均比较次数约 n²/2约 n²/4
内层循环每趟扫描整个未排序区间找到位置就停

这就是为什么 TimSort 在小数组上选择插入排序而不是冒泡排序。

常见问答

Q:什么是排序的稳定性?为什么重要?

稳定性是指:对于值相同的元素,排序后它们的相对顺序是否与排序前一致。

为什么重要? 当需要按多个字段排序时(例如先按成绩排序,再按姓名排序),稳定排序可以保留之前的排序结果。不稳定排序会破坏之前的顺序。

Q:为什么比较类排序的下界是 O(n log n)?

n 个元素有 n! 种排列,每次比较最多排除一半的可能性。因此至少需要 $\log_2(n!)$ 次比较,由 Stirling 公式可知 $\log_2(n!) = \Theta(n \log n)$。

Q:快速排序和归并排序如何选择?

维度快速排序归并排序
平均速度更快(常数因子小)稍慢
最坏情况O(n²)O(n log n)
稳定性不稳定稳定
额外空间O(log n)O(n)
缓存友好好(顺序访问)较差(需要额外数组)

结论:一般用快排,需要稳定性时用归并,需要最坏保证时用堆排序。

Q:为什么不用堆排序作为默认排序?

堆排序虽然最坏 O(n log n) 且原地,但缓存命中率低,实际速度比快排慢 2-3 倍。它的价值更多体现在优先队列这一数据结构上。

总结

没有"最好"的排序算法,只有"最合适"的。理解每种算法的特点和适用场景,才能做出正确选择。

记住这个口诀

  • 小规模用插入
  • 通用场景用快排
  • 要稳定用归并
  • 要保底用堆排
  • 值域小用计数

面试算法可视化图解