这道题用二分查找算法。
二分查找作为所有算法中,细节最多的算法之一,原因就在本题体现:
上一个题目中,我们用到的二分查找算法只是最基础的、最朴素的通用模板,而这个题需要用到寻找左边界和右边界的方法,难度增大很多,细节也很丰富,一不留神就会出错。
我们接下来就来看这个题目。
题目的要求是返回数组中,target的起始位置和结束为止,因为本题数组是非递减的,所以一共有三种情况:
- 连续target在数组中,例如 5 7 7 8 8 8 8 10 ,target = 8,那么需要我们返回的结果就是[4,8]
- 不连续的target在数组中,只需要返回两个相同的数字就完成,例如[4,4]
- 数组中不存在target或者数组为空,都返回[-1,-1]
这道题的大致思路为:用二分查找找到左边界,再用二分查找找到右边界,把两个边界拼起来就是需要的结果。也就是说,这个题需要用两次二分查找,两次二分查找的很多细节都是不一样的。
查找区间的左端点:
在这个“变形的”二分查找中,我们需要找的是左端点,也就是第一个3的地方。我们直接开始二分查找,x的值就是mid的值,我们的目标是t,于是我们就拿x的值和t作比较:
- x < t 此时让left = mid + 1 ,之后二分查找的范围就变成了[mid + 1,right]
- x >= t 此时让right = mid 即可,如果让right = mid - 1,会出现当t = x时,mid - 1直接到左边的区间去了。操作过后,二分查找的范围就变成了[left , mid]
此时,有两个细节问题:循环条件是什么?求中点的操作怎么求?
循环条件:
循环条件是 left <= right ,还是 left < right ?
- 当left = right 的时候,已经是最终结果了,无需再进行判断了
- 如果此时判断,就会陷入死循环
具体来说,可以分为三种情况:
判断的结果,我们判断的x值,是跟target相比较的,那么所有的情况就是这三种,对应了right和left的三种情况。当left = right的时候,就是最终结果。并且当第一种情况,再继续进行判断的时候,就会一直循环下去,进入 right = mid这个操作,会出现死循环。所以我们循环判断的条件就是:left < right
求中点的操作:
求中点有两种操作,这么写可以防溢出。两者的差别在于,在数组是偶数个的时候,是中间靠前的数字为中点还是中间靠后的数字为中点。
只能选择第一种,让中间靠前的数为中点。
当数组只有两个数的时候,前者和后者的中点是不同的。
如果是mid在前的情况,不管是 x < t ,left = mid + 1 还是 x >= t ,right = mid都会让left = right,此时终止循环。
如果是mid在后的情况, x < t ,left = mid + 1操作是没有问题的,但是如果是x >= t ,right = mid操作,会让right一直处于right = mid操作的死循环中。
所以求中点的操作,只能是 left + (right - left) / 2
此时,查找区间左端点的操作已经全部完成了。我们进入查找区间右端点的操作。
有了前面查找左端点的操作,查找右端点的操作就变得很简单了:
- 当x <= t 时,left = mid
- 当x > t 时,right = mid - 1
类似于把前面的操作对称一下。
循环条件:
仍然是left < right。
求中点的方式:
此时就变了一种方式,仍然是用前面的思想,此时变成 left + (right - left + 1) / 2 不会出现死循环,是符合题意的条件。
总结一下:二分查找左右边界的模板:
查找区间左端点的模板:
while(left < right){
int mid = left + (right - left) / 2;
if(...){
left = mid + 1;
}else{
right = mid;
}
}
查找区间右端点的模板:
while(left < right){
int mid = left + (right - left + 1) / 2;
if(...){
left = mid;
}else{
right = mid - 1;
}
}
通过两次二分查找,并且需要完善一下特殊的情况,我们的代码:
class Solution {
public int[] searchRange(int[] nums, int target) {
int[] ret = new int[2];
ret[0] = ret[1] = -1;
if(nums.length == 0){
return ret;
}
int left = 0,right = nums.length - 1;
while(left < right){
int mid = left + (right - left) / 2;
if(nums[mid] < target){
left = mid + 1;
}else{
right = mid;
}
}
if(nums[left] != target){
return ret;
}else{
ret[0] = right;
}
left = 0;
right = nums.length - 1;
while(left < right){
int mid = left + (right - left + 1) / 2;
if(nums[mid] <= target){
left = mid;
}else{
right = mid - 1;
}
}
ret[1] = left;
return ret;
}
}