二分搜索中 `right = mid` 而非 `right = mid + 1` 的解释

问题提出

在 Rust 的二分搜索实现中,经常看到如下代码:

rust 复制代码
while left < right {
    let mid = left + (right - left) / 2;
    if vec[mid] == target {
        return Some(mid);
    } else if vec[mid] < target {
        left = mid + 1;      // ✅ 目标在右半部分
    } else {
        right = mid;         // 🤔 为什么不是 right = mid + 1 ?
    }
}

很多人会有疑问:当 vec[mid] > target 时,为什么不直接设置 right = mid + 1?

核心答案:区间定义

这涉及到二分搜索的区间定义方式 。上述代码使用的是 左闭右开区间 [left, right)。

左闭右开区间 [left, right) 的特点

  • 搜索范围 :vec[left] 到 vec[right-1]
  • right 本身不在搜索范围内
  • 当 vec[mid] > target 时,目标只可能在 [left, mid-1] 范围内
  • 设置 right = mid 正好排除了 mid 及右侧所有元素

具体示例分析

rust 复制代码
// 假设 vec = [1, 3, 5, 7, 9], target = 3
// 初始: left = 0, right = 5

// 第一次迭代:
// mid = 0 + (5-0)/2 = 2
// vec[2] = 5 > 3, 所以目标在左半部分
  • 当前写法 (right = mid):right = 2,新范围 [0, 2)(即元素 1, 3)
  • 替代写法 (right = mid + 1):right = 3,新范围 [0, 3)(即元素 1, 3, 5)

两种写法在这个例子中都正确 ,但 right = mid 更精确,因为我们已经确定 mid 位置不需要再检查。

为什么 right = mid 更好?

1. 语义更清晰

  • right = mid 明确表示:"搜索范围缩小到 [left, mid)"
  • 正好排除了我们已经检查过的 mid 位置

2. 避免边界条件错误

考虑这个边界情况:

rust 复制代码
// vec = [1], target = 1
// left = 0, right = 1
// mid = 0
// vec[0] = 1 == target ✨ 找到目标!

如果使用 right = mid + 1,在某些实现中可能导致数组越界或逻辑错误。

3. 与 Rust 标准库保持一致

Rust 的标准库也使用左闭右开模式:

rust 复制代码
// 来自 Rust 标准库的类似实现思路
let mut base = 0;
let mut len = self.len();

while len > 0 {
    let mid = base + len / 2;
    // ... 逻辑处理
    len = mid - base;  // 类似 right = mid 的思想
}

4. 边界处理更安全

左闭右开区间有一个很好的性质:循环结束条件简单

rust 复制代码
while left < right {
    // 当 left == right 时,区间 [left, right) 为空
    // 搜索结束,无需额外处理
}

两种区间定义对比

区间类型 更新方式 搜索范围 优点 缺点
[left, right] 闭区间 right = mid - 1 [left, right] 直观易懂 需要处理 left == right 特殊情况
[left, right) 半开区间 right = mid [left, right) 边界清晰,不易出错 需要理解右开概念

数学证明

让我们用数学方式证明为什么 right = mid 是正确的:

假设在有序数组中,我们有:

  • vec[i] <= target 对于所有 i < mid
  • vec[mid] > target

那么 target 只可能在 [left, mid-1] 范围内。

由于我们使用 [left, right) 区间定义:

  • 要表示范围 [left, mid-1],我们需要 right = mid
  • 这样新区间 [left, mid) 正好包含 [left, mid-1]

常见误区澄清

❌ 误区 1:"right = mid 会漏掉元素"

实际上不会,因为:

  • mid 位置的元素已经检查过了
  • 新区间 [left, mid) 包含了所有可能的候选元素

❌ 误区 2:"right = mid + 1 更快"

两种写法的时间复杂度都是 O(log n),没有性能差异。

❌ 误区 3:"闭区间更直观"

虽然闭区间对初学者更直观,但半开区间在处理边界条件时更不容易出错。

实际代码验证

rust 复制代码
fn binary_search_correct(vec: &[i32], target: i32) -> Option<usize> {
    let mut left = 0;
    let mut right = vec.len();  // 注意:右开区间
    
    while left < right {
        let mid = left + (right - left) / 2;
        if vec[mid] == target {
            return Some(mid);
        } else if vec[mid] < target {
            left = mid + 1;
        } else {
            right = mid;  // ✅ 正确:排除 mid 及右侧
        }
    }
    None
}

// 测试各种情况
let tests = vec![
    (vec![], 1, None),
    (vec![1], 1, Some(0)),
    (vec![1, 3], 3, Some(1)),
    (vec![1, 3, 5], 1, Some(0)),
    (vec![1, 3, 5, 7, 9], 7, Some(3)),
];

for (arr, target, expected) in tests {
    assert_eq!(binary_search_correct(&arr, target), expected);
}

总结

使用 right = mid 而不是 right = mid + 1 是因为:

  1. 语义精确 :正好排除已检查的 mid 位置
  2. 边界安全:减少数组越界和逻辑错误风险
  3. 标准做法:符合计算机科学中的常见模式
  4. 性能相同:两种写法的时间复杂度都是 O(log n)
  5. 区间一致性:与左闭右开区间的定义保持一致

这种写法体现了左闭右开区间的优雅性,是二分搜索实现中的一个经典技巧,理解它有助于写出更 robust 的二分搜索代码。

扩展阅读


本文档解决了 "为什么二分搜索中 right = mid 而非 right = mid + 1" 的常见困惑

相关推荐
秋天的怀念_David34 分钟前
算法一千零一夜 · 第十四夜 幻方与奇妙的二进制
java·程序人生·算法
Flynt40 分钟前
Cloudflare 用 Rust 重写了 TypeScript:我拿同一批代码跑了跑,报错一模一样,退出码不一样
rust·typescript·前端工程化
薛定e的猫咪40 分钟前
(ICLR 2020)MAML 靠的不是快速学习,而是特征复用
人工智能·深度学习·学习·算法·机器学习
薛定e的猫咪1 小时前
(ICLR 2026)DF-META:面向 FJSP 的无导数元学习实例自适应调度
人工智能·深度学习·学习·算法·机器学习
小羊没烦恼!3 小时前
异步编程:线程概述及使用
java·开发语言·mysql·算法·c#
wanderist.3 小时前
202609CSP认证P3
算法
云淡风轻~窗明几净3 小时前
角谷猜想最接近严格证明的测试 Version1.1.1
人工智能·算法·机器学习·图论
机器学习之心4 小时前
PINN × SVM × NSGA-II × 熵权TOPSIS:一套跑通「建模—优化—决策」的多目标优化框架
算法·机器学习·支持向量机
别动我齐刘海4 小时前
机器人 C++ / 系统联调补强2——异常处理、Protobuf
linux·c++·人工智能·学习·算法·机器学习·机器人
荆棘鸟智能5 小时前
短历时洪水预报算法实践:水文模型与 LightGBM 残差订正融合
人工智能·算法·机器人