我最近在 Leetcode 刷了一些二分查找的题目,被虐得体无完肤。原以为二分查找很简单,只是套个模板就好。但是做了题目之后,就悟到了网上流传的一句话 "思路很简单,细节是魔鬼"。于是我费尽苦心,在知乎上找到一篇我觉得对二分查找分析得很透彻的文章,在这里分享一下。
本文章摘录于知乎 labuladong 的回答。
一、寻找一个数(基本的二分搜索)
int binarySearch(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) { //注意1
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1; //注意2
} else if (nums[mid] > target) {
right = mid - 1;
}
}
return -1;
}
1、循环条件
right 的 值为 nums.length - 1,即最后一个元素的索引。此时「搜索区间」为左闭右闭 [left, right]
while(left <= right) 的终止条件是 left == right + 1,写成区间的形式是 [right + 1, right] 。可见这时候区间为空,所以这时候 while 循环终止是正确的。
while(left < right) 的终止条件是 left == right,写成区间的形式是 [left, left],这时候区间非空,还有一个数 left。但此时 while 循环终止了,也就是说此时漏掉了一个索引 left。
2、区间收缩
因为 mid 已经搜索过,所以应该从「搜索区间」中去除,即 left = mid + 1 和 right = mid - 1。
二、寻找左侧边界的二分搜索
int binarySearch(int[] nums, int target) {
if (nums.length == 0) return -1;
int left = 0, right = nums.length; //注意1
while (left < right) { //注意2
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; //注意3
}
}
return left;
}
1、循环条件
right 的值为 nums.length,因此每次循环的「搜索区间」是左闭右开 [left, right)。
while (left < right) 的终止条件是 left == right,此时搜索区间 [left, left) 为空,所以可以正确终止循环。
2、区间收缩
因为「搜索区间」是左闭右开 [left, right),所以当 nums[mid] 被检测之后,下一步的搜索区间应该去掉 mid,分割成两个区间,即 [left, mid) 和 [mid + 1, right)。
为什么该算法能够搜索左侧边界?
答:关键在于对于 nums[mid] == target 这种情况的处理:
if (nums[mid] == target) {
right = mid;
}
可见,拿到 target 时不要立即返回,而是缩小搜索区间的上界 right,在区间 [left, mid) 中继续搜索,即不断向左搜索,达到锁定左侧边界的目的。
为什么返回 left 而不是 right?
答:都是一样的,因为 while 终止的条件是 left == right。
三、寻找右侧边界的二分搜索
public int binarySearch(int[] nums, int target) {
if (nums.length == 0) return -1;
int left = 0, right = nums.length;
while (left < right) {
int mid = left + (right - left) / 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;
}
1、循环条件
此处与寻找左侧边界相同。
2、区间收缩
因为「搜索区间」是左闭右开 [left, right),所以当 nums[mid] 被检测之后,下一步的搜索区间应该去掉 mid,分割成两个区间,即 [left, mid) 和 [mid + 1, right)。
为什么该算法能够搜索右侧边界
答:类似地,关键点还是这里:
if (nums[mid] == target) {
left = mid + 1;
}
当 nums[mid] == target 时,不要立即返回,而是增大「搜索区间」的下界 left,使得区间不断向右收缩,达到锁定右侧边界的目的。
为什么最后返回 left - 1?
if (nums[mid] == target) {
left = mid + 1;
// 这样想:mid = left - 1
}
也就是说,while 循环结束时, nums[left] 一定不等于 target 了,而 nums[left - 1] 有可能是 target。
注意事项
- 分析二分查找代码时,不要出现 else,全部展开 else if 方便理解。
- 注意「搜索区间」和 while 的终止条件。如果存在漏掉的元素,记得在最后检查。
- 如需要搜索左右边界,只要在 nums[mid] == target 时做修改即可。搜索右侧是需要减 1。