快速排序的外部接口和主函数:我们要实现的就是paration函数,进行一次排序后,返回插入元素的位置,然后利用分治的思想,将原来的元素序列分组排序;
cpp
void quickSort(int* arr,int left,int right)
{
if (left >= right)
{
return;
}
//pivot就是基准值的下标
int pivot = paration3(arr, left, right);
quickSort(arr, left, pivot - 1);
quickSort(arr, pivot+1, right);
}
int main() {
int arr[] = {2,3,4,1,6,9,0,5,7};
int len = sizeof(arr) / sizeof(arr[0]);
quicksort(arr, 0, len - 1);
// 打印排序结果
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
// 输出:0 1 2 3 4 5 6 7 9
return 0;
}
一、以最左边元素位置为标记位
(注:最右边元素在这里同理,就不反复介绍了。)
快速排序中 "以左边为思想" 通常指的是选择数组左边界的元素作为基准值(pivot),然后围绕这个基准值将数组分为 "小于基准" 和 "大于基准" 的两部分,再递归处理子区间。
核心思路(以左元素为基准)
- 选择基准 :直接将当前区间的左边界元素
arr[left]作为基准值(pivot)。 - 分区操作 :
- 用两个指针
i(左指针,从left+1开始)和j(右指针,从right开始)遍历区间。 - 左指针
i向右移动,找第一个大于基准值的元素; - 右指针
j向左移动,找第一个小于基准值的元素; - 若
i < j,交换arr[i]和arr[j],继续移动指针; - 当
i >= j时,将基准值arr[left]与arr[j]交换,此时基准值左边的元素都小于它,右边的元素都大于它(分区完成)。
- 用两个指针
- 递归处理 :对基准值左侧子区间(
left到j-1)和右侧子区间(j+1到right)重复上述过程,直到子区间长度为 1 或 0(递归终止)。
cpp
// 快速排序(以左边界元素为基准)
int paration1(int arr[], int left, int right) {
if (left >= right) return; // 递归终止条件:子区间长度为0或1
int pivot = arr[left]; // 选择左边界元素为基准值
int L = left + 1; // 左指针从基准右侧开始
int R = right; // 右指针从区间末尾开始
while (L < R) {
// 左指针找第一个大于基准的元素(注意:L不能超过right)
while (L <= right && arr[L] <= pivot) {
L++;
}
// 右指针找第一个小于基准的元素(注意:R不能小于left)
while (R >= left && arr[R] > pivot) {
R--;
}
// 若区间满足L < R,则交换两个元素,否则就是L和R重合,跳出循环;
if (L < R) {
int temp = arr[L];
arr[L] = arr[R];
arr[R] = temp;
}
}
// 分区完成,将基准值放到正确位置(基准值所在位置与R位置交换)
arr[left] = arr[R];
arr[R] = pivot;
return R;
}
二、左、中、右元素取中间值为标记位
由于快速排序排序与所选基准值密切相关,我们在上述最左元素位置的基础上,利用左中右元素大致的顺序进行取值,使得基准值不会特别极端。
"左中右元素比较思想"(我们通常也称作 "三数取中")是一种优化基准值(pivot)选择的策略。其核心是通过比较数组左边界(left)、中间位置(mid)、右边界(right) 三个元素的值,选择其中大小居中的元素作为基准值。这样可以避免因基准值过大或过小(如有序数组中选左 / 右元素为基准)导致的分区不平衡问题,从而优化快速排序的时间复杂度稳定性。
核心思路(三数取中)
-
确定三个位置:
- 左边界:
left(当前区间的起始索引); - 中间位置:
mid = left + (right - left) / 2(避免直接计算(left + right)/2可能的溢出); - 右边界:
right(当前区间的结束索引)。
- 左边界:
-
比较并选择基准 :比较
arr[left]、arr[mid]、arr[right]三个值,选择大小处于中间的元素作为基准值(pivot)。
注意:中间位置计算:mid = left + (right - left) / 2极为重要,原因:
在计算中间索引时,直接使用
(left + right) / 2可能导致整数溢出 ,尤其是当left和right都是较大的正数时。这是因为两个大整数相加的结果可能超过当前整数类型 (如 int) 所能表示的最大值,导致计算结果错误。具体举例(以 32 位
int类型为例)32 位
int的取值范围是 -2147483648 到 2147483647 (约 ±21 亿)。假设:left = 2147483640(一个接近最大值的正数),right = 2147483646(另一个接近最大值的正数)。直接计算
(left + right) / 2的问题:
先计算
left + right:2147483640 + 2147483646 = 4294967286。这个结果超过了 32 位int的最大值(2147483647),发生溢出。溢出后的结果:在有符号整数中,溢出是 "未定义行为",但实际编译中通常按 "补码环绕" 处理。4294967286 对应的 32 位补码值为
-2147483642(因为4294967286 - 2^32 = -2147483642)。计算中间索引:
(left + right) / 2 = (-2147483642) / 2 = -1073741821,这显然是错误的(中间索引不可能是负数,且与实际中间值偏差极大)。正确计算
left + (right - left) / 2如何避免溢出:
先计算
right - left:2147483646 - 2147483640 = 6(结果较小,不会溢出)。再计算
(right - left) / 2:6 / 2 = 3。最后加
left:2147483640 + 3 = 2147483643,这是正确的中间索引(处于left和right之间)。
代码:
cpp
// 三数取中:选择left、mid、right中值为基准,并交换到left位置
void medianOfThree(int arr[], int left, int right) {
int mid = left + (right - left) / 2; // 计算中间位置(避免溢出)
// 比较并交换,确保arr[left] <= arr[mid] <= arr[right]
if (arr[left] > arr[mid]) {
swap(&arr[left], &arr[mid]);
}
if (arr[left] > arr[right]) {
swap(&arr[left], &arr[right]);
}
if (arr[mid] > arr[right]) {
swap(&arr[mid], &arr[right]);
}
// 此时arr[mid]是中值,将其交换到left位置(作为基准)
swap(&arr[left], &arr[mid]);
}
int paration2(int* arr, int left, int right) {
assert(arr);
if (left >= right) return;
medianOfThree(arr, left, right);
int pivot = arr[left];
int L = left, R = right, mid = left + (right-left) / 2;
while (L < R) {
while (L<R && arr[R]>pivot) {
R--;
}
if (L < R) {
arr[L] = arr[R];
L++;
}
while (L < R && arr[L] < pivot) {
L++;
}
if (L < R) {
arr[R] = arr[L];
R--;
}
}
arr[R] = pivot;
return R;
}
三、快慢指针(单方向遍历)
核心思想
paration3 函数通过快慢指针 实现快速排序的分区操作,核心目标是:以数组左边界元素为基准值,将区间 [start, end] 划分为两大区域 ------
- 左侧(小于区域):所有元素均小于基准值;
- 右侧(大于等于区域):所有元素均大于或等于基准值。
最终将基准值移动到 "小于区域" 和 "大于等于区域" 的分界处,返回基准值的索引,为后续递归排序左右子区间提供依据。
快慢指针的角色与分工
分区过程依赖两个指针 prev(慢指针)和 cur(快指针),两者配合完成区域划分:
| 指针 | 角色定位 | 移动逻辑 |
|---|---|---|
prev |
慢指针,标记 "小于区域" 的右边界 | 初始位置为 start(基准值位置),仅当 cur 找到 "小于基准值" 的元素时,才向前移动一步(prev++),始终指向 "小于区域" 的最后一个元素。 |
cur |
快指针,负责遍历所有元素 "探路" | 从 start+1 开始,每次循环必向右移动一步(cur++),遍历区间内所有元素,检查当前元素是否需要纳入 "小于区域"。 |
cpp
int paration3(int* arr, int start, int end)
{
int prev = start;
int cur = start + 1;
int tmp = arr[start];
while (cur <= end)
{
if (arr[cur] < tmp)
{
prev++;
if (prev != cur)
{
swap(&arr[prev], &arr[cur]);
}
}
cur++;
}
swap(&arr[start], &arr[prev]);
return prev;
}
分区步骤详解
假设待分区数组为 arr,区间为 [start, end],基准值 tmp = arr[start](左边界元素),步骤如下:
-
初始化:
-
prev = start(初始时"小于区域"仅包含基准值本身); -
cur = start + 1(从基准值右侧第一个元素开始遍历)。
-
-
遍历与划分(
cur从start+1移动到end)**:对每个cur指向的元素arr[cur],判断其与基准值tmp的大小:-
若
arr[cur] < tmp(当前元素属于"小于区域"):先将
prev右移一步(prev++),扩大"小于区域"的范围;若
prev != cur(说明prev和cur之间存在"大于等于基准值"的元素),交换arr[prev]和arr[cur],将当前小元素"移入"小于区域。 -
若
arr[cur] >= tmp(当前元素属于"大于等于区域"):不做操作,
cur直接右移,当前元素留在右侧区域。
-
-
基准值归位 :当
cur遍历完所有元素(cur > end)后,prev指向"小于区域"的最后一个元素。此时交换arr[start](基准值)和arr[prev],基准值被移到分界处------左侧全为小于它的元素,右侧全为大于等于它的元素。 -
返回基准值索引 :返回
prev(此时prev是基准值的最终位置),用于后续递归排序[start, prev-1](左子区间)和[prev+1, end](右子区间)。
方法三总结:
- 高效性 :仅遍历一次区间(
cur从start+1到end),时间复杂度为O(n),空间复杂度为O(1)(原地操作,无需额外空间)。 - 逻辑简洁:通过快慢指针的 "探路 - 标记" 模式,避免了左右指针交叉判断的复杂逻辑,更易理解。
- 区域划分明确:严格区分 "小于基准值" 和 "大于等于基准值" 的区域,为后续递归排序提供清晰的子区间边界。
这种快慢指针的分区方式,是快速排序中 "单方向遍历划分" 的经典实现,核心在于通过指针的配合,将符合条件的元素逐步 "归位" 到目标区域。
以上就是快速排序常见的划分区域函数的几种方法,有任何问题,欢迎在评论区留言。