二分查找很常见,思路也很清晰。
但是还是那句经典的话: 思路越简单,细节越丰富。
二分查找 "恶心"人的地方有三个:
- while 里面是
left <= right
还是left < right
? - 缩小区间的时候,left 和 right 要不要带上mid?(最可能引起死循环的原因)
- 返回啥 ? left 还是 right ?
有时候凭感觉写的对了,有时候就错了,这就是传说中的7分天注定,3分靠感觉!
coding的最终奥义,无脑编程!
拿最常用的三个 二分查找场景:
- 寻找一个数
- 寻找一个数的最左侧出现的地方(左侧边界)
- 寻找一个数的最左侧出现的地方(右侧边界)
这三个场景其实可以抽象成两个:
- 找到这个数就返回
- 找到这个数不返回,继续往左/右找
废话不说,先上菜!
(我把所有可能产生疑问的地方标注出来单一在后面解释。)
case.1
int binary_search(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while(left <= right) {
int mid = ((right - left) >> 1) + left;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if(nums[mid] == target) {
return mid;
}
}
return -1; //直接返回查找失败
}
------------------------------------------------
case.2
int left_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
// note1
int mid = ((right - left) >> 1) + left;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1; // note2
} else if (nums[mid] == target) {
right = mid - 1; // note3 不返回,锁定左侧边界
}
}
// note4 最后检查 left 越界的情况
if (left >= nums.length || nums[left] != target)
return -1;
return left;
}
------------------------------------------------------
case.3
int right_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = ((right - left) >> 1) + left;
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;
}
note1: 什么时候取 <= ?
大多数情况下,如果你的left和right初始值都是有效的索引,就用 <= 。
其实,简单来说就是 right 取 nums.length - 1, 这就可以 <=,
这时的while结束条件是 left = right + 1;
这个知识点很重要!!!
对于后面两个题目,在 nums[mid]==target
时 start或者end的更新 跟这个点有很大关系!!
note2 & note 3: 更新start或者end的时候,带不带mid?
这里以第2题 寻找左边界举例子,第3题同理。
首先,note2这里 nums[mid] > target
时,target一定不可能在[mid, right]
这个闭区间出现了,所以 mid 自然取不到,right = mid - 1;
但是note3这里 为什么 nums[mid] == target
的时候 也不带mid了呢?
如果这里right = mid
的话,对于 1 2 2 2 3 搜 1,会死循环(自己模拟一下)
你可能会担心 取right=mid - 1
的话, mid左边如果不会再出现target了(此时的mid就是答案),就永远都回不到mid了,这不就找不到正确答案了吗?
实际上,不用担心,这就是为什么说上面 while结束条件是 left = right + 1;
很重要!
我们注意到 while 的终止条件:left<=right 更清晰一点的是退出while的条件是 left=right+1
也就是说,如果此时这个mid左边没有target了,这个right在被赋值完mid-1以后,就不会再更新(往左前进了)因为此时[left, right]闭区间的所有数都小于target,更新的一直是left,right会一直停在mid-1的位置。
left一直往右前进更新,直到 left=right 时进入循环,发现nums[mid]还是<target, left继续前进,变成right + 1,退出循环!
退出循环的时候,left = right + 1, right+1不就是mid吗!!!!
note 4 返回啥 ?
// note4 最后检查 left 越界的情况
if (left >= nums.length || nums[left] != target)
return -1;
return left;
left>= nums.length
对应 找的target比nums所有数都大,left一直前进;
nums[left] != target
对应,找的target比nums所有数都小,此时left一直指向0,right一直往左走;
简单点,nums[mid]==target
的时候,您更新的那个指针,就返回哪个!
所以。其实后两个问题归根结底还是因为
nums[mid]==target
的时候没有返回,而是继续搜索!!!
所以 此时的区间收缩不能按照上面nums[mid] > 或 < target
的情况理论判断,要结合实际!