二分查找的使用分类: 1)查找值,返回就行,没有其他要求 2)查找最左、最右边界的值(比如有重复的要求返回最左、最右下标)
下面从多个方面来进行分析:
1)终结条件:left <= right or left < right
首先我们要明白这两个的明确区别在哪里: 可以这么来说,二分查找的第一类使用 left <= right,第二类使用 left < right。Why?
接下来提供具体代码:
Normal (查找值):
Cpp
Copy
vector nums = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10 };
int target = 7;
int left = 0;
int right = nums.size() - 1;
while(left <= right) {
// 标准向下取整写法,防止溢出且清晰
int mid = left + (right - left) / 2;
if(nums[mid] > target)
right = mid - 1;
else if (nums[mid] < target)
left = mid + 1; // 【修正】原代码此处为 left = mid - 1,逻辑错误,应向右收缩
else
return mid;
}
target 是我们要找的目标值。
Specific (查找边界): 但如果我们要查找到 target 对应 vector<int> nums 的最左下标或者最右下标,我们如果使用上面的模板就会出错(因为找不到确切边界),所以我们要使用第二个模板;
Cpp
Copy
vector nums = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10 };
int target = 10;
// 要求找到最右下标
int left = 0;
int right = nums.size() - 1;
while(left < right) {
// 【修正】这里必须向上取整,配合 left = mid 防止死循环
int mid = left + (right - left + 1) / 2;
if(nums[mid] <= target)
left = mid; // 【修正】原代码 left = mid + 1 是找 upper_bound,此处改为找右边界逻辑
else
right = mid - 1;
}
return left; // 此时 left == right,即为最右下标
2)取整方式
取整方式分为向上取整、向下取整。 这个在查找的第一种情况(left <= right)是随意的,因为每次边界都会移动 mid + 1 或 mid - 1,不会卡住。但对应第二种情况(left < right)是有要求的,为什么?
下面以取最左下标为例: 如果我们想取到一个 target 最小下标,这就要求 我们如果取到 target 时不能直接跳过该位置。 此时我们可以让 right = mid,也可以让 left = mid。
- 关键逻辑 :
- 如果是
left = mid(左边界不移动),此时必须使用向上取整 (mid = left + (right - left + 1) / 2)。因为如果是向下取整,当left和right相邻时,mid会等于left,导致left不变,死循环。 - 如果是
right = mid(右边界不移动),此时必须使用向下取整 (mid = left + (right - left) / 2)。因为如果是向上取整,当left和right相邻时,mid会等于right,导致right不变,死循环。
- 如果是
可以看到的是,对于取到了一个值和 target 相同的情况,是通过改变另一个边界的大小来逼近的。同理对于取最右下标,我们同样使用 left = mid(配合向上取整)或 right = mid(配合向下取整)来应对。
3)返回值
就像我们在上个例子举得一样,你的返回值同样是会决定我们返回的正确性的。 在 left < right 的循环中,循环结束时 left == right。
- 如果是找左边界,循环结束后
left指向第一个>= target的位置。 - 如果是找右边界,循环结束后
left指向最后一个<= target的位置。 所以直接return left即可得到正确下标。
总结
我们在 left, right 是闭区间还是开区间,我个人偏爱闭区间 [left, right],因为对于开区间的话 right 是非法访问索引,这样的话就有很多地方都限制了我们。
对于一般情况(left <= right),无论是向上取整还是向下取整都是 OK 的。 但对于特殊情况(left < right 找边界):
- 找最左下标 :使用
left < right+ 向下取整 +right = mid。 - 找最右下标 :使用
left < right+ 向上取整 +left = mid。 这样应对的话一般是不会出错的。