Hoare 的两种写法

要不要我帮你把这段代码改写成可以直接运行的完整程序,方便你测试和验证?

“Hoare分区方案”的一个变种

void quickSort(int a[], int l, int r)
{
    if (l < r)
    {
        int i = l, j = r;
        int x = a[(l + r) / 2]; // 选择基准值
        while (i < j) // 没碰面
        {
            while (a[i] < x) ++i; // 左哨兵往右走
            while (a[j] > x) --j; // 右哨兵往左走
            // 如果i小于等于j,此时是小j的太小,小j的太小,要交换
            // 交换完继续走,i走到右边,j走到左边,确保没落下的
            if (i <= j) swap(a[i++], a[j--]);
        }
        quickSort(a, l, j); // j是左边最后一个
        quickSort(a, i, r); // i是右边第一个
    }
}

为了更清晰地理解它的定位,以下是详细的说明:

1. 核心分区方案:Hoare 分区法的变种

图中代码的核心思想与 Tony Hoare 爵士在1960年提出的原始分区方案 同源,主要特征是:

  • 双指针相向扫描:使用 ij 两个指针分别从数组两端向中间移动。
  • 基准值的选择:选择数组中间位置的元素作为基准值(x = a[(l+r)/2])。Hoare的原始方案通常选择第一个元素作为基准,但选择中间元素是常见的、用于优化已排序数组的变种。
  • 交换逻辑:当左指针找到不小于基准的元素,且右指针找到不大于基准的元素时,交换它们的位置。

所以,最准确的归类是:这是基于Hoare分区思想、并采用中间值作为基准的快速排序实现。

手写代码与 Hoare 分区方案 的核心算法逻辑一致(双指针从两端向中间扫描交换),但在实现结构和细节上存在差异


一、 关键区别对比

特征 手写代码 标准 Hoare 分区(教科书版)
指针初始化 i = l, j = r(指向两端) i = l-1, j = r+1(指向两端外侧)
循环结构 while (i < j) 外层包裹 while (true) 死循环,内部判断返回
指针移动时机 先扫描定位(while a[i]<x),交换后移动(i++, j--) 先移动(do i++),再比较,最后交换
终止条件 i >= j 时外层循环结束 i >= jreturn j
Bug 风险 swap(a[++i], a[j--]) 可能因参数求值顺序导致未定义行为 通常使用 swap(a[i], a[j]) 后单独移动指针

二、 流程差异图解

假设数组 [5, 4, 8, 2, 3],pivot = 8(中间元素)

手写代码执行流程

初始化: i=0(5), j=4(3), pivot=8

第1轮:
  i右移直到≥8 → 停在索引2(8)
  j左移直到≤8 → 停在索引4(3)  
  i(2) <= j(4) 成立,交换 a[2]↔a[4] → [5,4,3,2,8]
  i++, j-- → i=3, j=3

第2轮:
  i右移直到≥8 → 停在索引4(8)(但i<j即3<3不成立,实际上这轮不会执行...)
  【注意】此时 while(i<j) 即 3<3 不成立,退出

标准 Hoare 执行流程

初始化: i=-1, j=5, pivot=8

第1轮:
  do i++ → i=0(5)
  do j-- → j=4(3)
  swap(5,3) → [3,4,8,2,5]
  
第2轮:
  do i++ → i=1(4)
  do j-- → j=3(2)  
  swap(4,2) → [3,2,8,4,5]
  
第3轮:
  do i++ → i=2(8)
  do j-- → j=2(8)
  i >= j (2>=2) 成立,return j=2

三、 考试辨识要点(快速判断法)

1. 一眼识别 Hoare vs Lomuto

这是最重要的区分,考试中常考:

方案 扫描方向 递归参数 核心特征
Hoare 双指针,i从左→右,j从右→左 qsort(l, j)qsort(i, r) 两个 while 循环分别控制 i 和 j
Lomuto 单指针,i从左→右遍历 qsort(l, p-1)qsort(p+1, r) 维护 storeIndex,最后 swap(pivot, a[i])
挖坑法 单指针双向填补 类似 Hoare 用临时变量存 pivot,出现"坑"位

2. 识别 Hoare 的两种写法

看到双指针(i,j 从两端出发),还需判断是标准版还是变体版

标准版特征(do-while 结构):

i = l - 1; j = r + 1;
while (true) {
    while (a[++i] < x);  // 先移动再判断
    while (a[--j] > x);
    if (i >= j) break;   // 或 return j
    swap(a[i], a[j]);
}

变体版特征(while 嵌套结构,即手写代码):

i = l; j = r;
while (i < j) {          // 或 while (i <= j)
    while (a[i] < x) i++; // 先判断再移动
    while (a[j] > x) j--;
    if (i <= j) {         // 注意这里是 <=
        swap(a[i], a[j]);
        i++; j--;         // 交换后內缩
    }
}

四、 总结

手写代码本质上是 Hoare 分区的一种常见变体,逻辑正确(如果修正 swap 的参数问题),与标准版的区别主要在于指针移动与循环结构的组织方式。考试中只要看到双指针从两端向中间扫描交换,即可判定为 Hoare 分区思路,不必担心是标准版还是变体版,除非题目特别问细节。

特征 手写代码 标准 Hoare 分区(教科书版)
指针初始化 i = l, j = r(指向两端) i = l-1, j = r+1(指向两端外侧)
循环结构 while (i < j) 外层包裹 while (true) 死循环,内部判断返回
指针移动时机 先扫描定位(while a[i]<x),交换后移动(i++, j--) 先移动(do i++),再比较,最后交换
终止条件 i >= j 时外层循环结束 i >= jreturn j
Bug 风险 swap(a[++i], a[j--]) 可能因参数求值顺序导致未定义行为 通常使用 swap(a[i], a[j]) 后单独移动指针

2. 与另一种主流方案(Lomuto分区法)的对比

为了帮助您更好地区分,这里将图中的实现与另一种著名的、更简单的分区方案进行对比:

特性 图中的实现 (Hoare变种) Lomuto 分区法
指针方向 双指针,从两端相向移动 单指针,从一端同向移动
分区过程 通过交换使小元素左移、大元素右移 维护一个“小于基准”的区间,逐个元素放置
平均交换次数 更少,效率通常更高 更多
代码简洁性 相对复杂,需处理指针交错 非常简单直观
基准选择 通常为第一个或中间元素 通常为最后一个元素
图中的对应点 while (i < j) 和两个内层 while 循环 未使用

结论:图中的写法在效率上通常优于Lomuto方案,是工程中更受推崇的实现方式之一。

3. 图中代码的一个关键细节与常见误区

您需要特别注意图中代码的 第13行

if (i <= j) swap(a[++i], a[j--]); // 注意:这里是 ++i 和 j--

这里在交换的同时提前移动了指针。这种写法虽然紧凑,但会导致跳过当前已交换的元素,并可能使递归边界 (l, j)(i, r) 变得不那么直观,更容易出错。

更稳健、更推荐的写法是:

if (i <= j) {
    swap(a[i], a[j]); // 先交换
    i++; // 后移动指针
    j--;
}
posted @ 2026-01-28 19:07  kkman2000  阅读(63)  评论(0)    收藏  举报