快速排序:不仅仅是排序,是一场“站队”的艺术
很多初学者觉得快排难,是因为死记硬背 i、j 指针的移动。
其实,快排的核心只有一句话:
“找一个基准,让比它小的去左边,比它大的去右边,然后各回各家。”
1. 灵魂比喻:班主任排座位
想象一个乱糟糟的教室,班主任想按身高排座位,但他不想一个一个比,于是他想了个奇招:
- 选标杆: 随便抓起一个学生(比如叫“小明”),让他站到中间,他就是基准(Pivot) 。
- 大迁徙: 对全班说:“比小明矮的,全部跑去他左边;比小明高的,全部跑去他右边!”
- 定乾坤: 此时,小明的位置就彻底固定了!虽然左边一群人还没排好序,右边也乱,但小明已经站在了他这辈子最终该站的位置。
- 套娃操作: 班主任对左边那一群人说:“你们也抓个‘小明’出来,继续这么干!”
这就是快排:每一轮递归,本质上都是在给一个(或多个)元素寻找它“命定的终点”。
2. 深度拆解:分区(Partition)到底在干嘛?
这是快排最精彩的部分。我们用 C 语言最经典的 “双指针填坑法” 演示,这比单向扫描更直观。
核心逻辑:
假设数组是 [4, 7, 2, 5, 3],我们选第一个数 4 作为基准。
- 第一步:挖坑。 把
4拿出来存着,此时数组第一个位置空了(有个坑):[ (坑), 7, 2, 5, 3 ]。 - 第二步:右边找小。 右指针
j从末尾往左跳,发现3比4小。好,把3扔进刚才的坑里:[ 3, 7, 2, 5, (坑) ]。现在的坑到了右边。 - 第三步:左边找大。 左指针
i从左往右跳,发现7比4大。好,把7扔进右边的坑里:[ 3, (坑), 2, 5, 7 ]。现在的坑又回到了左边。 - 第四步:重复。 最终
i和j会相遇。 - 第五步:填回。 把当初存着的
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. 为什么快排“快”?(高手进阶视角)
如果你已经学过一遍,看这里能让你有新体验:
-
缓存友好性: 快排的指针移动是连续的内存访问,这对 CPU 缓存(Cache)极其友好,不像堆排序那样经常“跨时空”访问内存。
-
比较次数: 虽然最坏情况是 ,但在平均情况下,它的常数项极小。
-
原地操作(In-place): 它不需要申请额外的数组空间,对于内存受限的系统(比如你玩的 Debian 服务器或嵌入式环境)非常宝贵。
结语: 学习算法不要去记 i++ 还是 j--,要去想那个“坑”在哪里。坑在左边,就从右边找东西来填;坑在右边,就从左边找东西来填。最后相遇的地方,就是基准的家。