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

相关推荐
Pomelo_刘金2 小时前
Rust : 新版本 1.89.0
rust
Pomelo_刘金2 小时前
Rust : Trusted Publishing(受信发布)
rust
Pomelo_刘金2 小时前
Rust :裸函数 naked functions
rust·嵌入式
狮子也疯狂3 小时前
基于Django实现的智慧校园考试系统-自动组卷算法实现
python·算法·django
爱coding的橙子3 小时前
每日算法刷题Day84:11.11:leetcode 动态规划9道题,用时2h
算法·leetcode·动态规划
shenghaide_jiahu3 小时前
字符串匹配和回文串类题目
学习·算法·动态规划
有意义4 小时前
为什么说数组是 JavaScript 开发者必须精通的数据结构?
前端·数据结构·算法
努力努力再努力wz4 小时前
【Linux进阶系列】:线程(下)
linux·运维·服务器·c语言·数据结构·c++·算法
rit84324994 小时前
瑞利信道下PSK水声通信系统均衡技术
算法