从“站队”艺术看快速排序:这一次,我终于彻底理解了分区

5 阅读4分钟

快速排序:不仅仅是排序,是一场“站队”的艺术

很多初学者觉得快排难,是因为死记硬背 ij 指针的移动。 其实,快排的核心只有一句话: “找一个基准,让比它小的去左边,比它大的去右边,然后各回各家。”

1. 灵魂比喻:班主任排座位

想象一个乱糟糟的教室,班主任想按身高排座位,但他不想一个一个比,于是他想了个奇招:

  1. 选标杆: 随便抓起一个学生(比如叫“小明”),让他站到中间,他就是基准(Pivot)
  2. 大迁徙: 对全班说:“比小明矮的,全部跑去他左边;比小明高的,全部跑去他右边!”
  3. 定乾坤: 此时,小明的位置就彻底固定了!虽然左边一群人还没排好序,右边也乱,但小明已经站在了他这辈子最终该站的位置。
  4. 套娃操作: 班主任对左边那一群人说:“你们也抓个‘小明’出来,继续这么干!”

这就是快排:每一轮递归,本质上都是在给一个(或多个)元素寻找它“命定的终点”。


2. 深度拆解:分区(Partition)到底在干嘛?

这是快排最精彩的部分。我们用 C 语言最经典的 “双指针填坑法” 演示,这比单向扫描更直观。

核心逻辑:

假设数组是 [4, 7, 2, 5, 3],我们选第一个数 4 作为基准。

  • 第一步:挖坑。4 拿出来存着,此时数组第一个位置空了(有个坑):[ (坑), 7, 2, 5, 3 ]
  • 第二步:右边找小。 右指针 j 从末尾往左跳,发现 34 小。好,把 3 扔进刚才的坑里:[ 3, 7, 2, 5, (坑) ]。现在的坑到了右边。
  • 第三步:左边找大。 左指针 i 从左往右跳,发现 74 大。好,把 7 扔进右边的坑里:[ 3, (坑), 2, 5, 7 ]。现在的坑又回到了左边。
  • 第四步:重复。 最终 ij 会相遇。
  • 第五步:填回。 把当初存着的 4 填进最后那个坑:[ 3, 2, 4, 5, 7 ]

醍醐灌顶的一点: 此时 4 已经归位了!左边的 [3, 2] 和右边的 [5, 7] 独立去玩火。


3. C 语言黄金模板(双指针法)

我准备了 C 和 C++ 两个版本, 别的语言暂时因为能力不够,就没有展现,望大家见谅

C 语言版本

#include <stdio.h>

// 快速排序的核心:分区
int partition(int arr[], int low, int high) {
    int pivot = arr[low]; // 选第一个作为基准(挖坑)
    while (low < high) {
        // 从右向左找比基准小的
        while (low < high && arr[high] >= pivot) high--;
        arr[low] = arr[high]; // 填左坑
        
        // 从左向右找比基准大的
        while (low < high && arr[low] <= pivot) low++;
        arr[high] = arr[low]; // 填右坑
    }
    arr[low] = pivot; // 终极填坑
    return low;       // 返回基准的位置
}

void quickSort(int arr[], int low, int high) {
    if (low < high) {
        int mid = partition(arr, low, high); // 确定一个人的位置
        quickSort(arr, low, mid - 1);        // 搞定左边
        quickSort(arr, mid + 1, high);       // 搞定右边
    }
}

C++ 版本(利用 STL 增强可读性)

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

void quickSort(vector<int>& arr, int left, int right) {
    if (left >= right) return;

    int pivot = arr[left];
    int i = left, j = right;

    while (i < j) {
        while (i < j && arr[j] >= pivot) j--;
        while (i < j && arr[i] <= pivot) i++;
        if (i < j) swap(arr[i], arr[j]); // 左右各找一个不对劲的,互换
    }
    swap(arr[left], arr[i]); // 基准归位

    quickSort(arr, left, i - 1);
    quickSort(arr, i + 1, right);
}

4. 为什么快排“快”?(高手进阶视角)

如果你已经学过一遍,看这里能让你有新体验:

  1. 缓存友好性: 快排的指针移动是连续的内存访问,这对 CPU 缓存(Cache)极其友好,不像堆排序那样经常“跨时空”访问内存。

  2. 比较次数: 虽然最坏情况是 O(n2)O(n^2),但在平均情况下,它的常数项极小。

  3. 原地操作(In-place): 它不需要申请额外的数组空间,对于内存受限的系统(比如你玩的 Debian 服务器或嵌入式环境)非常宝贵。

结语: 学习算法不要去记 i++ 还是 j--,要去想那个“坑”在哪里。坑在左边,就从右边找东西来填;坑在右边,就从左边找东西来填。最后相遇的地方,就是基准的家。