🚀

快速排序

选一个"哨兵",小的站左边、大的站右边,然后两边各自再这么干

💡 打个比方

体育老师集合队伍:"报数!比我矮的站左边,比我高的站右边。"哨兵老师往中间一站,队伍立刻分成两堆:左边全是比哨兵矮的,右边全是比哨兵高的——哨兵的位置就此确定,再也不会动。

然后左边那堆里再挑一个哨兵,重复"矮左高右";右边那堆也一样。每选一个哨兵,就有一个同学站到了最终位置,剩下的问题变成两个更小的一半。

快速排序 = 选哨兵(pivot)+ 一次扫描把"比哨兵小的"换到左边、"大的"留在右边 + 递归排左右两半。它和归并排序一样平均 O(n log n),而且原地排序、不需要辅助数组——但哨兵选得不好(比如已经排好的数组还总拿第一个当哨兵),每轮只"切掉"一个,会退化成 O(n²)。

快排两件事:① 分治(divide & conquer):每轮至少一个元素(哨兵)落到最终位置;② 运气:哨兵越接近中位数,两边越平衡,越快;竞赛里用随机哨兵或"三数取中"防手气差。

🖼️ 大图看明白

① 一轮 partition:选 6 当哨兵,小的换到左边,大的留在右边 6 7 1 5 2 哨兵 = 6,i 指向"小区"开头,j 逐个扫描 6 1 7 5 2 j 看到 1 < 6:换进小区,i 前进 6 1 5 7 2 5 < 6:继续换进小区,7 留在右边 6 1 5 2 7 2 < 6:换入,扫描结束(7 留在右边) 2 1 5 6 7 哨兵 6 落到最终位置:左全小,右全大 左边 [2,1,5] 和右边 [7] 再各自递归快排——每轮至少有一个元素(哨兵)站到最终位置。 ② 快不快?哨兵手气决定一切 最好:每轮差不多对半切 log n 轮 × 每轮 O(n) = O(n log n) 最差:已排好 + 总拿第一个当哨兵 每轮只切掉一个,n 轮 = O(n²) 竞赛里防手气差:随机选哨兵,或"三数取中"。

📏 怎么写快排:只要 3 步

第 1 步:选哨兵——取 a[l](第一个元素)当哨兵。哨兵的职责是:一轮扫描后站到最终位置。

第 2 步:partition 扫描——指针 i 守住"小区"的边界(a[l+1..i] 都小于哨兵),j 从 l+1 扫到 r:a[j] < 哨兵就把 a[j] 和 a[i+1] 交换、i 前进一位;a[j] ≥ 哨兵就原地不动。扫完后小区 [l+1, i] 全小、[i+1, r] 全大。

第 3 步:放哨兵 + 递归——把哨兵和 a[i] 交换,哨兵落到最终位置 i;然后对 [l, i-1] 和 [i+1, r] 各自递归快排。基本情况:l >= r(区间长度 1)直接返回。

和归并排序的区别:归并"先拆后合",合并时要辅助数组;快排"边拆边定位置",原地交换、不需要辅助数组,但速度受哨兵手气影响(最差 O(n²))。

💻 C++ 怎么写

int a[105], n;
// partition:拿第一个当哨兵,小的换到左边
int partition(int l, int r) {
    int pivot = a[l];       // 哨兵
    int i = l;              // a[l+1..i] 都小于哨兵
    for (int j = l + 1; j <= r; j++) {
        if (a[j] < pivot) {
            int t = a[i+1]; a[i+1] = a[j]; a[j] = t;  // 小的换进小区
            i++;
        }
    }
    int t = a[l]; a[l] = a[i]; a[i] = t;  // 哨兵落到边界
    return i;               // 哨兵的最终位置
}
// 分治:左右两半各自递归
void quickSort(int l, int r) {
    if (l >= r) return;    // 基本情况:长度 1
    int p = partition(l, r);
    quickSort(l, p - 1);   // 左半(全比哨兵小)
    quickSort(p + 1, r);   // 右半(全比哨兵大)
}
考点:基本情况是 l >= r;partition 结束哨兵必在最终位置;平均 O(n log n),已排好 + 总拿第一个当哨兵时退化成 O(n²)(每轮只切掉一个);原地排序、不需要辅助数组。

✋ 你来试试

题 1:对 [4, 2, 7, 1] 跑快排(总拿第一个当哨兵),写出第一轮 partition 后的数组和哨兵位置,再数一数整个过程一共比较了几次?
第一轮:哨兵 4,i=0。j 扫到 2 < 4 → 2 已在小区,i 前进;j 扫到 7 ≥ 4 → 不动;j 扫到 1 < 4 → 和 a[i+1] 交换,得 [4,2,1,7],i 前进。最后哨兵 4 与 a[i] 交换 → [1, 2, 4, 7],哨兵落在下标 2(第 3 位),左边 [1,2] 全小、右边 [7] 全大(比较 3 次)。再排 [1,2]:哨兵 1,2 ≥ 1 不动,哨兵位置不变(比较 1 次)。总共 3 + 1 = 4 次比较,结果 [1,2,4,7]。
题 2:数组已经是 [1,2,3,…,n] 排好的,快排还总拿第一个当哨兵,为什么就退化成 O(n²)?怎么防?
已排好时第一个元素是最小值:一轮 partition 后它只能站最左边,"小区"为空、"大堆"有 n-1 个——每轮只切掉 1 个元素,要切 n 轮,每轮扫描 O(n),合起来 O(n²)。防法:随机选哨兵(坏运气概率骤降)或三数取中(首、中、尾三个里取中位数当哨兵),竞赛里还可以对小段切换插入排序。
🎯 一句话记住:快排 = 选哨兵 + partition(小左大右)+ 左右递归;每轮哨兵落到最终位置;平均 O(n log n)、最差 O(n²)(已排好 + 第一个当哨兵)、原地不占辅助数组。