快速排序的常见构思

快速排序的外部接口和主函数:我们要实现的就是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),然后围绕这个基准值将数组分为 "小于基准" 和 "大于基准" 的两部分,再递归处理子区间。

核心思路(以左元素为基准)

  1. 选择基准 :直接将当前区间的左边界元素 arr[left] 作为基准值(pivot)。
  2. 分区操作
    • 用两个指针 i(左指针,从 left+1 开始)和 j(右指针,从 right 开始)遍历区间。
    • 左指针 i 向右移动,找第一个大于基准值的元素;
    • 右指针 j 向左移动,找第一个小于基准值的元素;
    • i < j,交换 arr[i]arr[j],继续移动指针;
    • i >= j 时,将基准值 arr[left]arr[j] 交换,此时基准值左边的元素都小于它,右边的元素都大于它(分区完成)。
  3. 递归处理 :对基准值左侧子区间(leftj-1)和右侧子区间(j+1right)重复上述过程,直到子区间长度为 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) 三个元素的值,选择其中大小居中的元素作为基准值。这样可以避免因基准值过大或过小(如有序数组中选左 / 右元素为基准)导致的分区不平衡问题,从而优化快速排序的时间复杂度稳定性。

核心思路(三数取中)

  1. 确定三个位置

    • 左边界:left(当前区间的起始索引);
    • 中间位置:mid = left + (right - left) / 2(避免直接计算 (left + right)/2 可能的溢出);
    • 右边界:right(当前区间的结束索引)。
  2. 比较并选择基准 :比较 arr[left]arr[mid]arr[right] 三个值,选择大小处于中间的元素作为基准值(pivot)。

注意:中间位置计算:mid = left + (right - left) / 2极为重要,原因:

在计算中间索引时,直接使用 (left + right) / 2 可能导致整数溢出 ,尤其是当 leftright 都是较大的正数时。这是因为两个大整数相加的结果可能超过当前整数类型 (如 int) 所能表示的最大值,导致计算结果错误。

具体举例(以 32 位int类型为例)

32 位int的取值范围是 -2147483648 到 2147483647 (约 ±21 亿)。假设:left = 2147483640(一个接近最大值的正数),right = 2147483646(另一个接近最大值的正数)。

直接计算 (left + right) / 2 的问题

  1. 先计算 left + right2147483640 + 2147483646 = 4294967286。这个结果超过了 32 位int的最大值(2147483647),发生溢出

  2. 溢出后的结果:在有符号整数中,溢出是 "未定义行为",但实际编译中通常按 "补码环绕" 处理。4294967286 对应的 32 位补码值为 -2147483642(因为 4294967286 - 2^32 = -2147483642)。

  3. 计算中间索引:(left + right) / 2 = (-2147483642) / 2 = -1073741821,这显然是错误的(中间索引不可能是负数,且与实际中间值偏差极大)。

正确计算 left + (right - left) / 2 如何避免溢出:

  1. 先计算 right - left2147483646 - 2147483640 = 6(结果较小,不会溢出)。

  2. 再计算 (right - left) / 26 / 2 = 3

  3. 最后加 left2147483640 + 3 = 2147483643,这是正确的中间索引(处于 leftright 之间)。

代码:

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](左边界元素),步骤如下:

  1. 初始化

    1. prev = start(初始时"小于区域"仅包含基准值本身);

    2. cur = start + 1(从基准值右侧第一个元素开始遍历)。

  2. 遍历与划分( curstart+1 移动到 end)**:对每个 cur 指向的元素 arr[cur],判断其与基准值 tmp 的大小:

    1. arr[cur] < tmp(当前元素属于"小于区域"):

      先将 prev 右移一步(prev++),扩大"小于区域"的范围;

      prev != cur(说明 prevcur 之间存在"大于等于基准值"的元素),交换 arr[prev]arr[cur],将当前小元素"移入"小于区域。

    2. arr[cur] >= tmp(当前元素属于"大于等于区域"):

      不做操作,cur 直接右移,当前元素留在右侧区域。

  3. 基准值归位 :当 cur 遍历完所有元素(cur > end)后,prev 指向"小于区域"的最后一个元素。此时交换 arr[start](基准值)和 arr[prev],基准值被移到分界处------左侧全为小于它的元素,右侧全为大于等于它的元素。

  4. 返回基准值索引 :返回 prev(此时 prev 是基准值的最终位置),用于后续递归排序 [start, prev-1](左子区间)和 [prev+1, end](右子区间)。

方法三总结:

  1. 高效性 :仅遍历一次区间(curstart+1end),时间复杂度为 O(n),空间复杂度为 O(1)(原地操作,无需额外空间)。
  2. 逻辑简洁:通过快慢指针的 "探路 - 标记" 模式,避免了左右指针交叉判断的复杂逻辑,更易理解。
  3. 区域划分明确:严格区分 "小于基准值" 和 "大于等于基准值" 的区域,为后续递归排序提供清晰的子区间边界。

这种快慢指针的分区方式,是快速排序中 "单方向遍历划分" 的经典实现,核心在于通过指针的配合,将符合条件的元素逐步 "归位" 到目标区域。

以上就是快速排序常见的划分区域函数的几种方法,有任何问题,欢迎在评论区留言。

相关推荐
mit6.8242 小时前
背包dp|格雷码
算法
rit84324992 小时前
基于MATLAB的PCA+SVM人脸识别系统实现
人工智能·算法
RTC老炮2 小时前
webrtc降噪-NoiseEstimator类源码分析与算法原理
算法·webrtc
懒羊羊不懒@2 小时前
JavaSe—Stream流☆
java·开发语言·数据结构
不当菜鸡的程序媛3 小时前
Flow Matching|什么是“预测速度场 vt=ε−x”?
人工智能·算法·机器学习
sali-tec4 小时前
C# 基于halcon的视觉工作流-章58-输出点云图
开发语言·人工智能·算法·计算机视觉·c#
_OP_CHEN4 小时前
算法基础篇:(四)基础算法之前缀和
c++·算法·前缀和·蓝桥杯·acm·icpc·算法竞赛
_OP_CHEN4 小时前
算法基础篇:(五)基础算法之差分——以“空间”换“时间”
c++·算法·acm·icpc·算法竞赛·差分算法·差分与前缀和
DuHz4 小时前
霍夫变换和基于时频脊线的汽车FMCW雷达干扰抑制——论文阅读
论文阅读·物联网·算法·汽车·信息与通信·毫米波雷达