二分搜索相关的题目,大概有两类:一类是找值的,一类是找范围的。二分搜索的大概思路是很快能想出来的,但是细节方面很折磨人,比如以下几点:
- 返回
left还是right - 右边界是
nums.length还是nums.length - 1 while ( left ? right)是取<还是<=本文参考了 LeetCode 评论区的大佬的解法和labuladong的算法小抄,将对以上细节做补充,并应用到相应的题目。
查找一个数
这个场景是大家比较熟悉的,在一个排序的数组中查找一个数,找到就返回索引,没有就返回 -1,先来看模板
public int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length - 1; // 注意
// 注意 <=
while(left <= right) {
int mid = left + (right - left) / 2;
if(nums[mid] == target){
return mid;
} else if (nums[mid] < target){
left = mid + 1; // 注意
} else if (nums[mid] > target){
right = mid - 1; // 注意
}
}
return -1;
}
分析:
right的初始化值,影响 while 循环中left<right还是left<=rightright的初始化值也影响 right 应该更新为mid+1还是mid
1. 为什么 while 循环中是 <= 而不是 < ?
-
因为 right 初始化的值是
nums.length-1,即最后一个元素的索引,对应的是一个闭区间[left, right],这个闭区间中left是可以等于right的,所以使用<=。 -
如果 right 初始化的值是
nums.length,那么对应的是左闭右开区间[left, right),这个闭区间中left是不能等于right的,则应当使用<。
我们这个算法中使用的是前者 [left, right] 两端都闭的区间。这个区间其实就是每次进行搜索的区间。什么时候应该停止搜索呢,在找到目标值的时候可以停止搜素,但是如果没有找到目标值呢,就需要终止 while 循环,然后返回 -1
while(left <= right) 的终止条件是 left=right+1,写成区间是 [right+1, right],区间为空,此时终止 while 循环是正确的。如果使用while(left < right),终止条件是 left=right,写成区间是 [right, right],还有个 right 没有遍历到,此时直接终止循环就是错误的,如果硬是要使用 <,就需要在返回的时候做一下处理:
return nums[left] == target ? left : -1;
2. while 循环中 right 是等于 mid 还是 mid+1 ?
这个就是上面分析的第二点了:
- 当
right初始化为nums.length-1的时候,代表的是一个左闭右闭区间[left, right],在nums[mid]判断之后,下一个循环nums[mid]就不用再判断了,所以左边区间[left,mid-1],右边区间是[mid+1,right] - 当
right初始化为nums.length的时候,代表的是一个左闭右开区间[left, right),在nums[mid]判断之后,下一个循环nums[mid]就不用再判断了,但是因为右边是开,所以左边区间[left,mid),右边区间是[mid+1,right)
查找左边界
先看模板,尽量把三种 if 都列举出来,方便自己搞懂三种都是什么情况
public int left_bound(int[] nums, int target) {
if (nums.length == 0) return -1;
int left = 0;
int right = nums.length; // 注意
while (left < right) { // 注意
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
right = mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid; // 注意
}
}
return left;
}
同样的,while 循环 使用 < 还是 <= 取绝于 right 的取值,只是查找边界比较常用左闭右开的模板,用左闭右闭也是可以的。
退出循环的时候 left==right,所以返回 left 还是 right 是一样的
和上面查找某个值不同的是,当 nums[mid] == target 的时候,right=mid,查找的是左边界,所以往左靠,至于为什么是 right=mid,还是取绝于 right 的取值。
1.如果我就是要right = nums.length - 1呢?要怎么写
也是可以的,while 的终止条件应该是 left == right + 1,也就是其中应该用 <=:
public int left_bound(int[] nums, int target) {
if (nums.length == 0) return -1;
int left = 0;
int right = nums.length - 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;
} else if (nums[mid] > target) {
right = mid - 1; // 注意
}
}
// 检查出界情况
if (left >= nums.length || nums[left] != target){
return -1;
}
return left; // 为什么这个还是left呢,自己写一遍就知道了
}
因为退出循环的条件是 left == right + 1,所以当 target 比 nums 中所有元素都大时,会使得索引越界,所以要检查出界情况,也可以一开始就判断 target 是否大于数组中的最大元素,见下图
查找右边界
直接上代码,还是左闭右开的写法
public int right_bound(int[] nums, int target) {
if (nums.length == 0) return -1;
int left = 0, right = nums.length;
while (left < right) {
int mid = (left + right) / 2;
if (nums[mid] == target) {
left = mid + 1; // 注意
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid;
}
}
return left - 1; // 注意
}
为什么返回的结果是 left - 1 呢,自己写一遍就知道了,这个我也说不出个所以然,姑且记住吧。
同样给出 right = nums.length - 1 的写法
public int right_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// 这里改成收缩左侧边界即可
left = mid + 1;
}
}
// 这里改为检查 right 越界的情况,原因和查找左侧边界的相同
if (right < 0 || nums[right] != target)
return -1;
return right;
}
到这里,二分搜索算法就结束了,如果有不理解的可以看本文开头的链接(labuladong的算法小抄)。