为什么排序是算法第一课
不管你以后做什么方向——竞赛、后端、数据、AI——排序都是绕不开的基本功。排序是"把一串乱序数字变成有序"这个问题的标准化解法,每种排序背后都是一种算法思想。
学排序不是为了手写排序(竞赛里直接调 sort()),而是通过排序学会怎么分析一个算法的快慢、怎么用分治、怎么权衡时间和空间。
会写 sort() 不算本事,能讲清为什么 sort() 这么快才是本事。
- 冒泡/选择/插入O(n²) 入门三件套。
- 快排/归并O(n log n) 进阶。
- 时间复杂度算法快慢的尺子。
- 分治大问题拆小。
冒泡排序:最直白的交换
冒泡排序的思想:从左到右两两比较,前面比后面大就交换。一轮下来,最大的数像气泡一样"冒"到最后。
void bubbleSort(int a[], int n) {
for (int i = 0; i < n - 1; i++)
for (int j = 0; j < n - 1 - i; j++)
if (a[j] > a[j+1]) {
int t = a[j];
a[j] = a[j+1];
a[j+1] = t;
}
}
两轮循环嵌套,每轮比较 n 次,总共约 n²/2 次。这就是 O(n²)。
选择排序与插入排序
选择排序:每轮选最小的放到前面。n 轮,每轮扫一遍找最小,也是 O(n²)。
插入排序:像打扑克理牌——拿到新牌,从右往左找到它该插的位置。数据近乎有序时很快,最坏也是 O(n²)。
这三个 O(n²) 排序写法简单,但数据量一大就慢——10 万元素要算几亿次。
时间复杂度:O(n²) 和 O(n log n)
n=100000 时:
- O(n²) = 100 亿次操作,要几秒到几分钟。
- O(n log n) ≈ 100000 × 17 ≈ 170 万次,一瞬间。
差距是一万倍。这就是为什么竞赛里必须用 O(n log n) 的排序。
快速排序:分治的力量
快排的思想:选一个基准数 pivot,把比它小的放左边、比它大的放右边,再对左右两半递归快排。
void quickSort(int a[], int l, int r) {
if (l >= r) return;
int i = l, j = r, pivot = a[l];
while (i < j) {
while (i < j && a[j] >= pivot) j--;
a[i] = a[j];
while (i < j && a[i] <= pivot) i++;
a[j] = a[i];
}
a[i] = pivot;
quickSort(a, l, i - 1);
quickSort(a, i + 1, r);
}
每次分成两半,递归深度 log n,每层总共扫 n 次,所以平均 O(n log n)。
归并排序:稳定的 O(n log n)
归并排序另一种分治:先把数组切成两半递归排序,再把两个有序数组合并成一个。
归并排序的优点是稳定(相等元素相对顺序不变),缺点是要额外开一个临时数组。它的时间复杂度严格是 O(n log n),不像快排最坏会退化到 O(n²)。
- 冒泡两两交换 O(n²)。
- 快排分治平均 O(n log n)。
- 归并稳定 O(n log n)。
- 递归分而治之。
竞赛里为什么直接用 sort()
C++ STL 的 sort() 是快排的优化版(Introsort):默认快排,递归太深自动切到堆排,小数组切到插入排序。综合性能比手写任何排序都好。
#include <algorithm>
sort(a, a + n); // 升序
sort(a, a + n, greater<int>()); // 降序
竞赛里 99% 的排序需求直接调 sort()。但前提是:你得知道 sort() 背后在做什么,才能在需要定制比较、需要稳定排序时做出正确选择。
会调 sort() 是基本功,懂 sort() 是进阶功。