二分搜索中 `right = mid` 而非 `right = mid + 1` 的解释
问题提出
在 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及右侧所有元素
具体示例分析
// 假设 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. 避免边界条件错误
考虑这个边界情况:
// vec = [1], target = 1
// left = 0, right = 1
// mid = 0
// vec[0] = 1 == target ✨ 找到目标!
如果使用 right = mid + 1,在某些实现中可能导致数组越界或逻辑错误。
3. 与 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. 边界处理更安全
左闭右开区间有一个很好的性质:循环结束条件简单
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 < midvec[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:“闭区间更直观”
虽然闭区间对初学者更直观,但半开区间在处理边界条件时更不容易出错。
实际代码验证
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 是因为:
- 语义精确:正好排除已检查的
mid位置 - 边界安全:减少数组越界和逻辑错误风险
- 标准做法:符合计算机科学中的常见模式
- 性能相同:两种写法的时间复杂度都是 O(log n)
- 区间一致性:与左闭右开区间的定义保持一致
这种写法体现了左闭右开区间的优雅性,是二分搜索实现中的一个经典技巧,理解它有助于写出更 robust 的二分搜索代码。
扩展阅读
- Rust 标准库中的二分搜索实现
- 算法导论中的二分搜索分析
- 半开区间的数学性质
本文档解决了 “为什么二分搜索中 right = mid 而非 right = mid + 1” 的常见困惑
