二分搜索中 `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" 的常见困惑

相关推荐
xlq2232212 分钟前
22.多态(上)
开发语言·c++·算法
666HZ66614 分钟前
C语言——高精度加法
c语言·开发语言·算法
sweet丶22 分钟前
iOS MMKV原理整理总结:比UserDefaults快100倍的存储方案是如何炼成的?
算法·架构
星释22 分钟前
Rust 练习册 100:音乐音阶生成器
开发语言·后端·rust
云里雾里!1 小时前
力扣 209. 长度最小的子数组:滑动窗口解法完整解析
数据结构·算法·leetcode
CoderYanger2 小时前
递归、搜索与回溯-穷举vs暴搜vs深搜vs回溯vs剪枝:12.全排列
java·算法·leetcode·机器学习·深度优先·剪枝·1024程序员节
憨憨崽&2 小时前
进击大厂:程序员必须修炼的算法“内功”与思维体系
开发语言·数据结构·算法·链表·贪心算法·线性回归·动态规划
chem41113 小时前
C 语言 函数指针和函数指针数组
c语言·数据结构·算法
liu****4 小时前
八.函数递归
c语言·开发语言·数据结构·c++·算法
CM莫问4 小时前
详解机器学习经典模型(原理及应用)——岭回归
人工智能·python·算法·机器学习·回归