冒泡排序:简单但慢的入门选择
冒泡排序就像排队时老师让大家按身高重新站队,每次只比较相邻两人,发现顺序不对就交换。重复几轮后,队伍就排好了。代码写起来简单,适合教学场景。
void bubbleSort(int arr[], int n) {
for (int i = 0; i < n - 1; i++)
for (int j = 0; j < n - i - 1; j++)
if (arr[j] > arr[j + 1])
swap(&arr[j], &arr[j + 1]);
}
但它效率太低,时间复杂度是 O(n²),数据一多就卡得不行,实际开发中基本不会用。
快速排序:日常开发中的主力选手
快排像是分治法的代表人物,挑一个“基准数”,把数组分成两部分,左边都比它小,右边都比它大,然后递归处理左右两边。像整理快递包裹,先按区域分,再在区域内细分。
平均情况下性能很好,O(n log n),大多数语言内置的排序函数(比如 C++ 的 sort)底层都是快排或其变种。但最坏情况会退化到 O(n²),而且不稳定——相同值的元素可能被打乱顺序。
归并排序:稳定可靠的高性能方案
归并排序走的是“拆分到底再合并”的路线。先把数组一分为二,各自排好序,再把两个有序数组合并成一个。这个过程像拼乐高,先搭好两个模块,再严丝合缝地拼在一起。
它的优势在于稳定且始终是 O(n log n),适合对稳定性要求高的场景,比如数据库排序、外部排序。缺点是需要额外的存储空间,内存消耗比快排大。
堆排序:利用堆结构实现高效排序
堆排序基于完全二叉树的性质,先把数组构建成一个最大堆,然后一次次取出堆顶元素放到末尾,调整堆结构继续下一轮。整个过程像不断从一堆文件中拿出最紧急的那个去处理。
它不需要额外空间,时间复杂度稳定在 O(n log n),但常数项较大,实际运行速度通常不如快排。而且也不稳定,相同元素的相对位置可能会变。
插入排序:小数据量下的灵活选择
插入排序就像打扑克时理牌,每摸一张牌就把它插到合适的位置。对于已经接近有序的数据,插入排序非常快,甚至能达到 O(n)。
虽然整体复杂度也是 O(n²),但在小规模数据(比如几十个元素)或者作为其他高级排序的“收尾工具”时表现不错。Python 的 Timsort 就结合了归并和插入的优点。
几种排序算法核心特性一览
以下是常见排序算法的关键指标对比:
| 算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 是否稳定 |
|---|---|---|---|---|
| 冒泡排序 | 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²) | O(n²) | O(1) | 是 |
怎么选?看场景说话
如果你在写一个小型工具程序,数据量不大,插入排序反而更轻便;系统级开发追求速度,快排是首选;需要保证相同元素顺序不变,比如用户积分排行榜并列情况要按注册时间排序,就得上归并排序。
现代语言的标准库排序大多不是单一算法,而是混合策略。比如 Java 的 Arrays.sort() 对基本类型用双轴快排,对象类型则用 Timsort,兼顾性能和稳定性。
理解这些算法的区别,不只是为了应付面试,更是在面对真实业务问题时能做出合理技术选型。比如日志系统做离线分析时要排序海量数据,这时候可能就得考虑外排序结合归并的方式,而不是死磕内存里的快排。