以下是最常见的代码形式,其中的标记是需要注意的细节:
int right_bound(int[] nums, int target) {
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、为什么这个算法能够找到右侧边界?
答:类似地,关键点还是这里:
if (nums[mid] == target) {
left = mid + 1;
当 nums[mid] == target
时,不要立即返回,而是增大「搜索区间」的左边界 left
,使得区间不断向右靠拢,达到锁定右侧边界的目的。
2、为什么最后返回 left - 1
而不像左侧边界的函数,返回 left
?而且我觉得这里既然是搜索右侧边界,应该返回 right
才对。
答:首先,while 循环的终止条件是 left == right
,所以 left
和 right
是一样的,你非要体现右侧的特点,返回 right - 1
好了。
至于为什么要减一,这是搜索右侧边界的一个特殊点,关键在锁定右边界时的这个条件判断:
// 增大 left,锁定右侧边界
if (nums[mid] == target) {
left = mid + 1;
// 这样想: mid = left - 1
因为我们对 left
的更新必须是 left = mid + 1
,就是说 while 循环结束时,nums[left]
一定不等于 target
了,而 nums[left-1]
可能是 target
。
至于为什么 left
的更新必须是 left = mid + 1
,当然是为了把 nums[mid]
排除出搜索区间,这里就不再赘述。
3、为什么没有返回 -1 的操作?如果 nums
中不存在 target
这个值,怎么办?
答:只要在最后判断一下 nums[left-1]
是不是 target
就行了,类似之前的左侧边界搜索,做一点额外的判断即可:
while (left < right) {
// ...
}
// 判断 target 是否存在于 nums 中
// left - 1 索引越界的话 target 肯定不存在
if (left - 1 < 0 || left - 1 >= nums.length) {
return -1;
}
// 判断一下 nums[left - 1] 是不是 target
return nums[left - 1] == target ? (left - 1) : -1;
4、是否也可以把这个算法的「搜索区间」也统一成两端都闭的形式呢?这样这三个写法就完全统一了,以后就可以闭着眼睛写出来了。
答:当然可以,类似搜索左侧边界的统一写法,其实只要改两个地方就行了:
int right_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// 这里改成收缩左侧边界即可
left = mid + 1;
}
}
// 最后改成返回 left - 1
if (left - 1 < 0 || left - 1 >= nums.length) {
return -1;
}
return nums[left - 1] == target ? (left - 1) : -1;
}
当然,由于 while 的结束条件为 right == left - 1
,所以你把上述代码中的 left - 1
都改成 right
也没有问题,这样可能更有利于看出来这是在「搜索右侧边界」
int right_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
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 || right >= nums.length) {
return -1;
}
return nums[right] == target ? right : -1;
}
最后,奉上大佬的代码可视化
我写了首诗,把二分搜索算法变成了默写题 | labuladong 的算法笔记
最后,总结我们前面学习的二分算法细节问题: 4. 逻辑统一