二分搜索相关的题目,大概有两类:一类是找值的,一类是找范围的。二分搜索的大概思路是很快能想出来的,但是细节方面很折磨人,比如以下几点:
- 返回
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<=right
right
的初始化值也影响 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的算法小抄
)。