引言
想象一下,当你打开手机通讯录寻找一个朋友时,当你在电商网站按价格筛选商品时,或者当搜索引擎在毫秒间为你呈现最相关的结果时------排序,这个看似简单的操作,正在悄无声息地支撑着现代数字生活的每一次高效交互。
排序算法是计算机科学中最基础、最核心的领域之一,其重要性远超表面所见。
在众多排序算法中,快速排序占据着独特的位置。自1960年由英国计算机科学家托尼·霍尔发明以来,它以其优雅的"分而治之"策略和卓越的平均性能,成为实际应用中最广泛使用的排序算法。从C语言的qsort()到Java的Arrays.sort(),从Python的sorted()到浏览器JavaScript引擎,快速排序的身影无处不在。
这是排序专题的第一篇内容,我们将以托尼·霍尔发明的快速排序作为开篇。
一、快排原理
整体逻辑
- 快速排序用的是分而治之的思想,以从小到大的顺序为例,我们要做以下工作:
- 1、在整个区间中选取一个基准key。
- 2、把基准放在它该在的位置上,把比它小的数放左边,比它大的数放右边。
- 3、对这个基准左边的区间和右边的区间再分别进行快速排序。
- 如上,我们可以先写个大框架:
c
void QuickSort(int* a, int n)
{
if (有序了)
return;
int key; //选一个基准
_QuickSort(); //假设这是单趟排序的逻辑
QuickSort(左区间);
QuickSort(右区间);
}
单趟逻辑
- 在单趟排序中,我们一般把基准值放在第一个,这样方便代码实现
- 之后,我们定义一个left和一个right,分别从两头遍历数组。
- right先走,找到比key小的值就停下;left再走,找比key大的值,两个都停下后,交换left和right的值。
- 一直重复此过程,直到left和right相遇,此时相遇位置的值比key小,且相遇位置是key应该在的位置,交换相遇位置与key的值
- 此时,key在它该在的位置上,比它小的数都在它左边,比它大的数都在它右边,单趟排序完成。
- 最后再对key的左右区间递归,递归完成后排序结束。
为什么相遇位置的值比key小?
相遇位置有两种情况
- 1、left遇到right:我们是让right先走,如果是left遇到right,说明是right先停下,而right停下的条件是找到比key小的值,left与right相遇后位置的值就是比key小的值。
- 2、right遇到left:上一趟left找到比key大的值之后与上一趟的right找到的比key小的值交换,所以上一趟的left存的是比key小的值。而right遇到left,说明right没有遇到比key小的值,直接与上一趟的left相遇,此时相遇位置也是比key小的值。
二、代码实现
c
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
int partion(int a[], int left, int right)
{
int key = a[left];
int i = left, j = right;
while(i<j)
{
while ( i < j && a[j] >= key)//右边找小
j--;
while (i < j && a[i] <= key)//左边找大
i++;
if(i<j)
Swap(&a[i], &a[j]);//找到交换(若i=j,则意味着没有两个都找到就相遇了)
}
Swap(&a[left], &a[i]);//交换相遇位置与key的值
return i;
}
//快速排序
void QuickSort(int a[], int left, int right)
{
if (left >= right)
return;
int mid = partion(a, left, right);//记录key的值
//对左右区间递归
QuickSort(a, left, mid-1);
QuickSort(a,mid+1,right);
}
三、快排优化
- 根据上面的代码,我们知道:快排的Hoare法是用递归实现的,那么如果递归深度太深,就容易造成栈溢出。
那么我们看这样一种场景:假如对一堆正序的n个数据排序,我们先选择第一个数为key,此时第一个数已经在它的位置上了,因为是正序的数据。
然后我们划分左右区间递归,左区间没有,右区间是除了key的n-1个数。

- 这时候再进行第二层排序,左区间依旧没有,右区间是除了key的n-2个数。
以此推类,我们需要n层栈帧才能完成这次排序,效率极低。
- 为了解决快排的一系列问题,我们可以做出很多优化:
1. 随机选 key
- 在排序区间内随机选取一个元素作为 key,能有效避免数据分布带来的最坏情况,但随机数生成本身有一定开销。
cpp
int GetRandomKey(int left, int right) {
srand(time(NULL)); // 需确保只调用一次
return rand() % (right - left + 1) + left;
}
int PartSort(int* a, int left, int right) {
int keyi = GetRandomKey(left, right);
Swap(&a[keyi], &a[left]); // 将 key 换到最左边,保持原有分区逻辑
// 后续分区逻辑不变 ...
}
2. 三数取中法(最推荐)
- 取区间 left, right 的第一个元素、中间元素、最后一个元素,比较三者大小,选择值居中的元素作为 key。这种方法能有效避免极端情况,且无需额外开销。
cpp
int GetMidIndex(int* a, int left, int right) {
int mid = (left + right) / 2;
if (a[left] < a[mid]) {
if (a[mid] < a[right])
return mid; // left < mid < right
else if (a[left] < a[right])
return right; // left < right < mid
else
return left; // right < left < mid
} else { // a[left] >= a[mid]
if (a[left] < a[right])
return left; // mid < left < right
else if (a[mid] < a[right])
return right; // mid < right < left
else
return mid; // right < mid < left
}
}
int PartSort(int* a, int left, int right) {
int midi = GetMidIndex(a, left, right);
Swap(&a[midi], &a[left]); // 将中位数换到最左边
// 后续分区逻辑不变 ...
}

取三个数: 3, 9, 8
排序后: 3, 8, 9
中间值: 8 ← 选作 key(比 3 更接近中位数)
3. 小区间优化(递归降级)
- 当子区间规模小于某个阈值时,不再使用递归,而是直接调用插入排序。插入排序在小数据量下常数小、效率高,同时减少了递归深度。
c
void QuickSort(int* a, int left, int right) {
// 小区间优化:数据量小于 10 时改用插入排序
if (right - left + 1 < 10) {
InsertSort(a + left, right - left + 1);
return;
}
// 继续快排逻辑...
}
//阈值选择建议: 一般取 10 ~ 20 之间
//太大会浪费快排的优势,太小则优化效果不明显。
4.三路划分(处理大量重复数据)
当数组中有大量重复元素时,传统的左右分区方法会将所有等于 key 的元素集中在同一侧,导致分区不均衡。三路划分将数组分为三部分:
left, less → 小于 key
less+1, great-1 → 等于 key
great, right →大于 key
c
void QuickSortThreeWay(int* a, int left, int right) {
if (left >= right) return;
int key = a[left];
int less = left;
int great = right;
int i = left + 1;
while (i <= great) {
if (a[i] < key) {
Swap(&a[less], &a[i]);
less++;
i++;
} else if (a[i] > key) {
Swap(&a[i], &a[great]);
great--;
} else {
i++;
}
}
// 递归处理小于 key 和大于 key 的部分
QuickSortThreeWay(a, left, less - 1);
QuickSortThreeWay(a, great + 1, right);
}
- 适用场景: 数组中大量重复值(如成绩统计、性别分类等),可大幅提升效率。
四、非递归快排
- 我们能否模拟压栈与出栈,来实现一个不需要递归的快速排序呢?
- 当然可以,具体做法是:使用显式栈模拟递归过程,将需要处理的区间压入栈中,循环取出处理。
c
void QuickSortNonR(int* a, int left, int right) {
Stack st;
StackInit(&st);
StackPush(&st, right);
StackPush(&st, left); // 先压左,后压右,取时先右后左
while (!StackEmpty(&st)) {
int begin = StackTop(&st);
StackPop(&st);
int end = StackTop(&st);
StackPop(&st);
int keyi = PartSort(a, begin, end);
// [begin, keyi-1] 和 [keyi+1, end] 入栈
if (keyi + 1 < end) {
StackPush(&st, end);
StackPush(&st, keyi + 1);
}
if (begin < keyi - 1) {
StackPush(&st, keyi - 1);
StackPush(&st, begin);
}
}
StackDestroy(&st);
}