二分查找那些坑
1. 二分搜索:寻找一个数
如下两种实现方式,right 边界的取值,直接影响着算法的实现细节,这些细节稍不小心就会犯错。二分查找看似简单,实则不然,真正能考虑到所有细节,把代码写准确是很不容易的。
int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length - 1; // 注意,搜索区间是[left, right],左闭右闭
while(left <= right) {// 注意,此处是<=, 因为搜索区间左闭右闭,left==right也应该考虑进来
int mid = (right + left) / 2;
if(nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1; // 注意,搜索区间是左闭右闭,mid已经被搜索过了,此处直接mid+1
else if (nums[mid] > target)
right = mid - 1; // 注意,搜索区间是左闭右闭,mid已经被搜索过了,此处直接mid-1
}
return -1;
}
int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length; // 注意,搜索区间是[left, right),左闭右开
while(left < right) {// 注意,此处是<, 因为搜索区间左闭右开,left==right不应该考虑
int mid = (right + left) / 2;
if(nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1; // 注意,搜索区间是左闭右开,left边界会被搜索,此处left=mid+1
else if (nums[mid] > target)
right = mid; // 注意,搜索区间是左闭右开,此处不能令right = mid - 1,因为right边界不会被搜索,mid-1会被missed掉,所以必须是right = mid。
}
return -1;
}
1. while 循环的条件中 <=,< 有什么区别?
答:如果初始化 right 的赋值是 nums.length-1,即最后一个元素的索引,相当于两端都闭区间 [left, right],则 while 循环的条件中 用<=。如果初始化 right 是 nums.length,相当于左闭右开区间 [left, right),则 while 循环的条件中 <,因为索引大小nums.length 是越界的。
2. 为什么第一种算法是 left = mid + 1,right = mid - 1,第二种算法是 left = mid + 1,right = mid ,如何判断?
答:这也是二分查找的一个难点,不过只要你能理解前面的内容,就能够很容易判断。刚才明确了「搜索区间」这个概念,如果算法的搜索区间是两端都闭的,即 [left, right]。那么当我们发现索引 mid 不是要找的 target 时,如何确定下一步的搜索区间呢?
当然是 [left, mid - 1] 或者 [mid + 1, right] ,因为 mid 已经搜索过,应该从搜索区间中去除。相应的,如果算法的搜索区间是左闭右开的,即 [left, right),则当mid 不是要找的 target 时,下一步的搜索区间是 [left, mid) 或者 [mid + 1, right)。
2. 二分搜索:寻找一个数的左侧边界
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) / 2;
if (nums[mid] == target) {
right = mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid; // 注意 不是mid - 1
}
}
// target 比所有数都大
if (right == nums.length) return -1;
// 类似之前算法的处理方式
return nums[right] == target ? right : -1;
}
1. 为什么该算法能够搜索左侧边界?
答:关键在于对于 nums[mid] == target 这种情况的处理:
if (nums[mid] == target) {
right = mid;
}
可见,找到 target 时不要立即返回,而是缩小「搜索区间」的上界 right,在区间 [left, mid)中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。另外,为什么返回 right 而不是 left?其实返回哪个都一样,因为 while 终止的条件是 left == right。
3. 二分搜索:寻找一个数的右侧边界
int right_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) / 2;
if (nums[mid] == target) {
left = mid + 1; // 注意
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid; // 注意
}
}
if (left == 0) return -1;
return nums[left-1] == target ? (left-1) : -1;
}
1. 为什么这个算法能够找到右侧边界?
答:类似地,关键点还是这里:
if (nums[mid] == target) {
left = mid + 1;
}