二分查找
需要关注的点
-
left
与right
的初始化 -
left < right
还是left <= right
-
区间应该是
[left, right]
还是[left, right)
- 左闭右闭:两个边界上的元素都可以取到
- 左闭右开:左边界上的元素可以取到,右边界上的元素无法取到
-
循环中如何控制边界
- 例如:
r = mid
还是r = mid - 1
- 例如:
关注点解析
-
left <= right
适用于区间为[left, right]
。- 例如:
[
1
,
1
]
[1, 1]
[1,1],
left = 1, right = 1
, 1 1 1 是搜索集合中的一部分,需要进行判断。
- 例如:
[
1
,
1
]
[1, 1]
[1,1],
-
left < right
则适用于区间为[left, right)
。 如果是[left, right]
, 会出现矛盾- 例如: [ 1 , 1 ) [1, 1) [1,1),左右边界都是 1 1 1 ,而右边界是开区间无法取到该值,所以矛盾。
-
在循环中需要对由
left
和right
得出的中间值mid
进行判断,例如nums[mid] > target
,说明应在mid
左边的区间寻找答案,这时就需要考虑right
应该更新成mid
还是mid - 1
。-
如果是
[left, right]
的情况下,nums[mid]
是严格大于target
的,也就是说nums[mid]
不是正确答案,故将right
更新为mid - 1
,也就是right = mid - 1
。 -
如果是
[left, right)
的情况下,也就是右区间取不到的情况下,right
值取mid
,也就是right = mid
-
-
区间为
[left, right]
时,left = 0
、right = nums.length - 1
-
区间为
[left, right)
时,left = 0
、right = nums.length
。right = nums.length
是因为区间的右边界取不到。
两种写法
以leetcode第704题为例:704. 二分查找
写法一
public int search(int[] nums, int target) {
int l = 0, r = nums.length - 1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (nums[mid] > target) {
r = mid - 1;
} else if (nums[mid] < target) {
l = mid + 1;
} else {
return mid;
}
}
return -1;
}
写法二
public int search(int[] nums, int target) {
// 注意这时r的取值,表示[left, right)区间,右边界无法取到
int l = 0, r = nums.length;
while (l < r) {
int mid = l + r >> 1;
if (nums[mid] > target) {
r = mid;
} else if (nums[mid] < target) {
l = mid + 1;
} else {
return mid;
}
}
return -1;
}
更加精简的模板
这是在AcWing上学习到的模板。
首先需要知道,区间都是[left, right]
, 而循环条件是 left < right
。初始值: left = 0
、right = length - 1
。
在区间只剩下一个元素的时候,例如:
[
1
,
1
]
[1, 1]
[1,1],这时 l == r
, 循环终止,但是区间中还剩下一个元素,所以退出循环后需要对这个元素进行额外判断。
模板一
public int search(int[] nums, int target) {
int l = 0, r = nums.length - 1;
while (l < r) {
int mid = l + r >> 1;
if (nums[mid] >= target) r = mid;
else l = mid + 1;
}
return nums[l] == target ? l : -1;
}
模板二
这个模板与上面有不同的地方,就是 l = mid
、r = mid - 1
。
但是会出现死循环的问题,例如:
[
1
,
2
]
[1, 2]
[1,2],l = 0, r = 1
, target = 1
, mid = l + r / 2 = 0
, 执行if (nums[mid] <= target) l = mid;
, l
再次被更新为
0
0
0, r
还是
1
1
1 。这就导致了l
永远无法与 r
相等,导致循环无法结束。
public int search(int[] nums, int target) {
int l = 0, r = nums.length - 1;
while (l < r) {
int mid = l + r + 1 >> 1;
if (nums[mid] <= target) l = mid;
else r = mid - 1;
}
return nums[l] == target ? l : -1;
}
两个模板的补充:
假设有这样一个区间:
[
1
,
2
,
2
,
2
,
3
]
[1,2,2,2,3]
[1,2,2,2,3] ,target = 2
,需要找到
2
2
2 第一次出现的位置,还有
2
2
2 最后一次出现的位置。也就是找到
[
2
,
2
,
2
]
[2,2,2]
[2,2,2] 的左边界和右边界。
- 通过 模板一 可以用来找到左边界,这是因为
r = mid
, 也就是区间的右边界有向左缩的趋势,最终剩下的一个元素就是目标值的起始位置。 - 同理,通过 模板二 可以用来找到边界,这是因为
l = mid
, 也就是区间的左边界有向右缩的趋势,最终剩下的一个元素就是目标值的结束位置。
细节补充
计算mid时,l + r / 2
或者 l + r >> 1
可能会导致溢出,所以可以使用这种写法:l + (r - l) / 2
或者 l + (r - l) >> 1
。
如果使用的是Java语言的话,可以直接使用 l + r >>> 1
这种写法,>>>
表示无符号右移,即使 l + r
溢出了也可以通过无符号右移一位的方式得到正确结果,Arrays.binarySearch()
中就使用的 >>>
进行除 2 操作。