文章目录
- 一、区间的定义与特点
- 二、循环条件与跳出逻辑
-
- [1. 左闭右开区间 `[left, right)`](#1. 左闭右开区间
[left, right)) - [2. 闭区间 `[left, right]`](#2. 闭区间
[left, right]) - [3. 开区间 `(left, right)`](#3. 开区间
(left, right))
- [1. 左闭右开区间 `[left, right)`](#1. 左闭右开区间
- 三、完整代码示例
-
- [1. 左闭右开区间 `[left, right)`](#1. 左闭右开区间
[left, right)) - [2. 闭区间 `[left, right]`](#2. 闭区间
[left, right]) - [3. 开区间 `(left, right)`](#3. 开区间
(left, right))
- [1. 左闭右开区间 `[left, right)`](#1. 左闭右开区间
- 四、总结与建议
二分查找(Binary Search)是计算机科学中最经典的算法之一,广泛应用于排序数组的查找问题。然而,二分查找的边界处理却常常让人困惑,尤其是关于区间的定义(左闭右开、闭区间、开区间)以及循环条件和更新逻辑的选择。本文将详细讲解这些问题,并推荐一种最佳实践,重点分析为什么要这样做。
一、区间的定义与特点
在二分查找中,区间的定义决定了算法的实现方式。常见的区间定义有以下三种:
- 左闭右开区间 :
[left, right)- 特点 :包含
left,不包含right。 - 适用场景 :适合处理数组下标范围为
[0, n)的情况。 - 跳出条件 :
left == right。 - 为什么这样定义 :
- 左闭右开的定义使得右边界
right始终保持为开区间,避免了数组越界的风险。 - 逻辑清晰,循环条件
while (left < right)直接反映了区间是否为空。 - 更新逻辑简单,
right = mid或left = mid + 1都能保证区间逐步缩小。
- 左闭右开的定义使得右边界
- 特点 :包含
- 闭区间 :
[left, right]- 特点 :包含
left和right。 - 适用场景:更直观,尤其是数学背景的开发者更容易理解。
- 跳出条件 :
left > right。 - 为什么这样定义 :
- 闭区间的定义使得左右边界都参与查找,逻辑上更符合"包含所有可能值"的直觉。
- 循环条件
while (left <= right)使得每次都能检查完整的区间。
- 特点 :包含
- 开区间 :
(left, right)- 特点 :不包含
left和right。 - 适用场景:较少使用,因为实现复杂且不直观。
- 跳出条件 :
left + 1 == right。 - 为什么这样定义 :
- 开区间的定义适合一些特殊场景,但由于边界不参与查找,逻辑较为复杂,实际应用中较少使用。
- 特点 :不包含
二、循环条件与跳出逻辑
循环条件的选择与区间定义密切相关。以下是三种区间定义下的循环条件与跳出逻辑,并解释为什么这样设计:
1. 左闭右开区间 [left, right)
- 循环条件 :
while (left < right)- 原因 :
- 右边界
right是开区间,不参与查找。 - 当
left == right时,区间为空,循环结束。
- 右边界
- 原因 :
- 跳出条件 :
left == right,此时区间为空。 - 更新逻辑 :
- 如果目标值在左半部分:
right = mid- 原因 :
mid仍然可能是目标值,因此保留mid,缩小右边界。
- 原因 :
- 如果目标值在右半部分:
left = mid + 1- 原因 :
mid已经被检查过,排除mid,缩小左边界。
- 原因 :
- 如果目标值在左半部分:
2. 闭区间 [left, right]
- 循环条件 :
while (left <= right)- 原因 :
- 右边界
right是闭区间,参与查找。 - 当
left > right时,区间为空,循环结束。
- 右边界
- 原因 :
- 跳出条件 :
left > right,此时区间为空。 - 更新逻辑 :
- 如果目标值在左半部分:
right = mid - 1- 原因 :
mid已经被检查过,排除mid,缩小右边界。
- 原因 :
- 如果目标值在右半部分:
left = mid + 1- 原因 :
mid已经被检查过,排除mid,缩小左边界。
- 原因 :
- 如果目标值在左半部分:
3. 开区间 (left, right)
- 循环条件 :
while (left + 1 < right)- 原因 :
- 开区间不包含边界,当
left + 1 == right时,区间为空,循环结束。
- 开区间不包含边界,当
- 原因 :
- 跳出条件 :
left + 1 == right,此时区间为空。 - 更新逻辑 :
- 如果目标值在左半部分:
right = mid- 原因 :
mid仍然可能是目标值,因此保留mid,缩小右边界。
- 原因 :
- 如果目标值在右半部分:
left = mid- 原因 :
mid已经被检查过,排除mid,缩小左边界。
- 原因 :
- 如果目标值在左半部分:
三、完整代码示例
以下是三种区间定义的完整代码实现:
1. 左闭右开区间 [left, right)
cpp
int binarySearch(vector<int>& nums, int target) {
int left = 0, right = nums.size(); // [left, right)
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1; // 排除 mid
} else {
right = mid; // 保留 mid
}
}
return -1; // 未找到
}
2. 闭区间 [left, right]
cpp
int binarySearch(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1; // [left, right]
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1; // 排除 mid
} else {
right = mid - 1; // 排除 mid
}
}
return -1; // 未找到
}
3. 开区间 (left, right)
cpp
int binarySearch(vector<int>& nums, int target) {
int left = 0, right = nums.size(); // (left, right)
while (left + 1 < right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid; // 保留 mid
} else {
right = mid; // 保留 mid
}
}
return -1; // 未找到
}
四、总结与建议
在实现二分查找时,建议严格按照区间的定义来选择循环条件和更新逻辑:
- 左闭右开区间 :使用
while (left < right),更新逻辑为left = mid + 1或right = mid。 - 闭区间 :使用
while (left <= right),更新逻辑为left = mid + 1或right = mid - 1。 - 开区间 :使用
while (left + 1 < right),更新逻辑为left = mid或right = mid。
为什么推荐左闭右开区间?
- 逻辑清晰:右边界始终不参与查找,避免数组越界。
- 易于理解 :
while (left < right)直接反映了区间是否为空。 - 广泛使用 :许多标准库(如 C++ 的
std::lower_bound)采用左闭右开的定义。
强行混用循环条件和区间定义会导致逻辑复杂化,增加代码的理解难度和错误风险。因此,推荐大家按照区间定义的方式去写循环条件,并确保跳出循环的逻辑与区间定义一致。