首页/数据结构/07-sorting/快速排序 🔗 在 Obsidian 中打开
数据结构 · 07-sorting

快速排序

重要度 ★★★★★ 数据结构/排序排序交换排序分治法408考研
速查
快排 = 选枢轴partition 分左右递归排。平均 $O(n\log n)$、最坏 $O(n^2)$、空间 $O(\log n)$、不稳定。408 必考代码。

速查表

项目
核心思想选枢轴 → partition 分左右 → 递归排
平均时间$O(n\log n)$
最坏时间$O(n^2)$(已排序 + 首元素作枢轴)
空间$O(\log n)$ 递归栈,最坏 $O(n)$
稳定性❌ 不稳定
考试频率⭐⭐⭐⭐⭐ 必考

核心概念

快速排序(Quick Sort)是由 Tony Hoare 在 1960 年提出的一种基于分治法的排序算法,是目前内部排序中综合性能最好的算法之一。

核心思想:从待排序序列中选取一个元素作为枢轴(pivot),通过一趟排序将序列划分为左右两个子序列——左子序列中所有元素 $\leq pivot$,右子序列中所有元素 $> pivot$。然后对左右子序列递归执行相同操作,直到整个序列有序。

快速排序的排序过程类似于构建二叉排序树(BST):每次选取一个根节点,将比它小的放左边、比它大的放右边。因此快速排序的递归树结构与 BST 高度相关。

关键特性

  • 快速排序是交换排序的一种,但它不是简单地两两交换,而是通过一趟划分确定一个元素的最终位置
  • 每趟排序会确定一个(或多个)元素的最终位置
  • 快速排序不产生有序子序列(区别于冒泡排序和插入排序)
  • 是内部排序中平均性能最好的算法
  • 原地排序(in-place):不需要额外数组空间,仅需递归栈空间
递归树 ≈ 二叉排序树:每个节点是一趟划分确定的枢轴 49 27 76 13 38 65 97 第 1 趟 第 2 趟
图:递归树高度 = 递归深度 = 空间复杂度的量级;树越平衡越快。

关键性质

性质说明
最好时间复杂度$O(n\log n)$,每次 pivot 恰好将序列等分为两半
平均时间复杂度$O(n\log n)$,约 $1.39n\log n$ 次比较
最坏时间复杂度$O(n^2)$,序列本身有序或逆序,每次只减少一个元素
空间复杂度最好/平均 $O(\log n)$,最坏 $O(n)$(递归栈深度)
稳定性❌ 不稳定

详细分析

  • 最好情况:每次划分都能均匀分割,递归树高度为 $\lfloor\log_2 n\rfloor+1$,每层处理 $O(n)$ 个元素,总时间为 $O(n\log n)$
  • 最坏情况:初始序列有序或逆序,每次划分只产生一个非空子序列,递归树退化为链表,高度为 $n$,总时间为 $O(n^2)$
  • 平均情况:经过数学证明,平均比较次数约为 $2n\ln n \approx 1.39n\log_2 n$
  • 空间复杂度 $= O(递归深度)$,递归深度等于递归树的高度

算法步骤

一趟划分(Partition)过程

  1. 选取序列中某个元素作为枢轴 pivot(通常选第一个元素 L[low]
  2. 初始化两个指针:low 指向序列开头,high 指向序列末尾
  3. 先从右往左扫描:当 high 指向的元素 $\geq pivot$ 时,high--;否则将 L[high] 移到 low 位置
  4. 再从左往右扫描:当 low 指向的元素 $\leq pivot$ 时,low++;否则将 L[low] 移到 high 位置
  5. 重复步骤 3-4,直到 low == high
  6. 将 pivot 放入 low == high 指向的位置,此时 pivot 已到达最终位置

递归过程

  1. 对 pivot 左边的子序列 [low, pivot-1] 递归执行快速排序
  2. 对 pivot 右边的子序列 [pivot+1, high] 递归执行快速排序
  3. 递归终止条件:low >= high(子序列长度 $\leq 1$)

代码实现(双边循环法)

int Partition(int A[], int low, int high) {
    int pivot = A[low];  // 选取第一个元素作为枢轴
    while (low < high) {
        // 从右往左找比 pivot 小的元素
        while (low < high && A[high] >= pivot)
            high--;
        A[low] = A[high];
        // 从左往右找比 pivot 大的元素
        while (low < high && A[low] <= pivot)
            low++;
        A[high] = A[low];
    }
    A[low] = pivot;  // pivot 放到最终位置
    return low;
}

void QuickSort(int A[], int low, int high) {
    if (low < high) {
        int pivotpos = Partition(A, low, high);
        QuickSort(A, low, pivotpos - 1);
        QuickSort(A, pivotpos + 1, high);
    }
}
⚠️ 必须先从 high 端扫描!如果先从 low 端扫描,当 lowhigh 相遇时,相遇位置的元素可能大于 pivot,导致划分错误。

优化版本:三数取中法选枢轴

int MedianOfThree(int A[], int low, int high) {
    int mid = low + (high - low) / 2;
    if (A[low] > A[mid])  swap(A[low], A[mid]);
    if (A[low] > A[high]) swap(A[low], A[high]);
    if (A[mid] > A[high]) swap(A[mid], A[high]);
    swap(A[mid], A[high-1]);  // 中位数放到 high-1 位置
    return A[high-1];
}

更多优化见 quick-sort-optimization

手算示例

对序列 49, 38, 65, 97, 76, 13, 27, 49* 进行快速排序(首元素作枢轴):

第一趟划分($pivot = 49$):

初始:  49  38  65  97  76  13  27  49*
       ↑low                            ↑high

从右扫描: 27 < 49, high 停在 27
       27  38  65  97  76  13  [27] 49*
       ↑low                      ↑high

从左扫描: 65 > 49, low 停在 65
       27  38  [65]  97  76  13  65  49*
           ↑low              ↑high

从右扫描: 13 < 49, high 停在 13
       27  38  13  97  76  [13]  65  49*
           ↑low        ↑high

从左扫描: 97 > 49, low 停在 97
       27  38  13  [97]  76  97  65  49*
               ↑low  ↑high

low == high, 放入 pivot:
       27  38  13  [49]  76  97  65  49*
               ↑pivot

结果:$pivot=49$ 到达最终位置,左子表 {27, 38, 13},右子表 {76, 97, 65, 49*}

第二趟:对左子表 {27, 38, 13}($pivot=27$)和右子表 {76, 97, 65, 49*}($pivot=76$)分别划分。

继续递归直到所有子序列长度 $\leq 1$。最终排序结果:13, 27, 38, 49, 49*, 65, 76, 97

观察排序后 49 排在 49* 之前看似"稳定",但这只是巧合——快排的元素跨距离搬移不保证相等元素的相对次序。

常见考法

考法解题套路
手动模拟一趟划分过程给出初始序列,写出每一趟划分后的结果
判断是否为快速排序某一趟的结果已知一趟排序后有 $n$ 个元素到达最终位置
快速排序与初始序列的关系有序 / 逆序时性能最差
快速排序的改进方法三数取中法、随机化枢轴
快速排序与其他排序的比较平均性能最好,但不稳定
递归次数与初始序列的关系与初始序列和枢轴选择有关,与分区处理顺序无关
一趟排序确定的元素个数每趟划分确定 1 个元素(枢轴)的最终位置

易错点

高频错判
  1. 认为快速排序是稳定的快速排序不稳定
  2. 认为快速排序在任何情况下都最快有序 / 逆序时最慢 $O(n^2)$
  3. ❌ 忘记划分时先从 high 端开始扫描 → 必须先从右往左扫描,否则会出错
  4. ❌ 混淆"趟"和"次划分" → 快速排序的每一趟只进行一次划分(确定一个枢轴的最终位置),整个排序由多趟组成
  5. 认为空间复杂度始终是 $O(\log n)$最坏情况下是 $O(n)$
  6. 认为快速排序适合链表不适合,需要随机访问
  7. 认为每趟排序确定的元素是连续的可能不连续

核心结论

必背快速排序是内部排序中平均性能最好的算法,平均时间复杂度 $O(n\log n)$
  1. 初始序列有序或逆序时性能最差 $O(n^2)$
  2. 快速排序不稳定
  3. 空间复杂度 $O(递归深度)$,最好 $O(\log n)$,最坏 $O(n)$
  4. 每趟排序确定一个元素的最终位置
  5. 排序过程类似于构建二叉排序树
  6. 优化关键:选取好的枢轴(三数取中法、随机化)
  7. 408 中最重要的排序代码,务必熟练背诵

记忆卡片

快排的平均 / 最坏时间复杂度?
平均 $O(n\log n)$,最坏 $O(n^2)$。最坏情况发生在序列有序或逆序时。
快排是否稳定?给反例。
不稳定。反例:序列 [3, 3*, 2],排序后 3 与 3* 的相对位置可能改变。
为什么必须先从 high 端扫描?
因为 pivot 取的是 A[low]。若先从 low 端扫描,两指针相遇处的元素可能大于 pivot,导致划分错误。
快排空间复杂度取决于什么?
递归深度。树高从 $\lfloor\log_2 n\rfloor+1$(最好)到 $n$(最坏),故 $O(\log n)\sim O(n)$。
如何优化枢轴选择?
① 三数取中(首、中、尾的中位数);② 随机选枢轴;③ 小子表切换插入排序。目的是避免最坏情况。
每趟划分确定几个元素位置?
1 个——枢轴。整个排序由多趟组成,已定位的元素不一定连续。

交互动画 · 一趟划分 Partition

pivot = 49 双边循环法:先右后左,交替搬运到"坑"里 low high
点击「播放」或「下一步」开始演示
序列:49 38 65 97 76 13 27 49*
示意图:橙色格 = 本步被搬运的元素,虚线格 = 空出的"坑",绿色格 = pivot 落位。low(橙)与 high(绿)指针交替逼近,相遇即划分完成。

相关知识点

merge-sort bubble-sort heap-sort

↑ 以上为站内 HTML 相对链接(纯网页可浏览);本页右上「在 Obsidian 中打开」跳回源笔记。