写在前面
- 文章是在前人的基础上进行总结整理再加上自己的一点理解,仅作为自己学习的记录,不作任何商业用途!
- 如果在文章中发现错误或者侵权问题,欢迎指出,谢谢!
基本框架
public int binarySearch(int[] array, int target) {
int left = 0;
int right = ...;
while(...) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) ...;
else if (nums[mid] < target) ...;
else if (nums[mid] > target) ...;
}
return ...;
}
- 说明
- 二分查找的时候,不要出现 else,而是把所有情况用 else if 写清楚,这样可以清楚地展现所有细节
- 上面代码中
...标记的部分,就是可能出现细节问题的地方,即不同的二分类型的题这部分内容可能不同
寻找一个数
- 在数组中搜索一个数,如果该数存在则返回下标,不存在则返回 -1
public int binarySearch(int[] array, int target) {
int left = 0;
int right = array.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (array[mid] == target) {
return mid;
} else if (array[mid] < target) {
left = mid + 1;
} else if (array[mid] > target) {
right = mid - 1;
}
}
return - 1;
}
-
Q1:为什么
while循环的条件中是<=,而不是<?- 首先第一点:可以看到 right = array.length - 1,那么就表明取值是可以取到 right 这个下标值的,也即二分搜索的区间为 [left, right],因此 left 可以等于 right
- 再者第二点:明确何时会退出循环
- 找到 target 值,也即通过
array[mid] == target条件直接return mid - 没有找到 target 值,那么只能是通过
left > right来退出循环,即left = right + 1,此时肯定不存在一个 [right + 1, right] 这样的区间的,所以可以直接return -1
- 找到 target 值,也即通过
- 最后第三点:在
right = array.length - 1不变的情况下,修改为while (left < right),怎么才能得到正确的答案?- 此时
while退出循环的条件是:一个是找到 target,直接return mid,另一个是没有找到 target,通过left = right退出循环 - 注意此时 left = right 退出循环的时候的「搜索区间」为 [left, left],是没有去判断 array[left] 是否等于 target 的,所以在返回的时候打一个补丁就行:
return array[left] == target ? left : -1 - 完整代码在变形代码 1
- 此时
-
Q2:为什么
left = mid + 1,right = mid - 1?- 可以看出在
while循环里面有三个 if 判断,明显的将数组分为三个区间:小于、等于、大于,那么当我们发现索引mid不是要找的target时,下一步应该去搜索哪里呢? - 当然是去搜索
[left, mid - 1]或者[mid + 1, right],因为mid已经搜索过了,需要排除掉 - 像那种
right = mid或者left = mid的是将数组分为两个区间的解题方式
- 可以看出在
-
Q3:如果是 right = array.length 和 while (left < right) 这种写法该如何修改代码?
- 在修改了 right 和 while 循环的条件的情况下,如何能够得到正确的结果,其重点在于退出循环的分析
- 在该条件下,推出循环的情况分为两种:
- 找到 target 值,也即通过
array[mid] == target条件直接return mid - 没有找到 target 值,那么只能是通过
left = right来退出循环,注意这个情况退出循环的分析:- 此时
left = right退出循环的时候的「搜索区间」为[left, left),是没有去判断array[left]是否等于 target 的,所以需要增加一个判断 - 此时
left = right是有可能等于 array.length 的,因此还需要判断数组越界的情况
- 此时
- 找到 target 值,也即通过
- 完整代码在变形代码 2
-
Q3:此算法有什么缺陷?
- 比如说给你有序数组
nums = [1,2,2,2,3],target 为 2,此算法返回的索引是 2,没错 - 但是如果我想得到 target 的左侧边界,即索引 1,或者我想得到 target 的右侧边界,即索引 3,这样的话此算法是无法处理的
- 这种在实际开发中还是很常见的,那么怎么修改上面的算法得到左侧或者右侧的边界呢?见下文
- 比如说给你有序数组
-
变形代码 1
public int binarySearch(int[] array, int target) {
int left = 0;
int right = nums.length - 1;
while(left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
}
else if (nums[mid] < target) {
left = mid + 1;
}
else if (nums[mid] > target) {
right = mid; // 注意这里也发生了变化
}
}
return nums[left] == target ? left : -1;
}
- 变形代码 2
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length;
while(left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
}
else if (nums[mid] < target) {
left = mid + 1;
}
else if (nums[mid] > target) {
right = mid; // 后面会讲为什么是 mid 而不是 mid - 1
}
}
if (left == nums.length || nums[left] != target) return -1;
return left;
}
寻找左边界
- 在一个存在重复数字的数组中搜索一个数,如果该数存在则返回最左边的下标,不存在则返回 -1
public int leftBound(int[] nums, int target) {
int left = 0;
int right = nums.length; // 注意和下一版本对比
while(left < right) { // 注意和下一版本对比
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; // 注意和下一版本对比
}
}
if (left == nums.length || nums[left] != target) return -1;
return left;
}
-
Q1:为啥这里要写成
right = nums.length使得「搜索区间」变成左闭右开呢?- 因为对于搜索左右侧边界的二分查找,这种写法比较普遍
- 后面也提供了
right = nums.length - 1的版本,并且使用该方式,需要注意改动的地方
-
Q2:为什么 while 中是
<而不是<=?- 因为
right = nums.length而不是nums.length - 1。因此每次循环的「搜索区间」是[left, right)左闭右开的形式 - 在该条件下,是通过
left = right来退出循环,注意这个情况退出循环的分析:- 此时
left = right退出循环的时候 是没有判断array[left]是否等于 target 的,所以需要去判断 - 此时
left = right是有可能等于 array.length 的,因此还需要判断数组越界的情况
- 此时
- 因为
-
Q4:为什么
left = mid + 1,right = mid?和之前的算法不一样?- 因为我们的「搜索区间」是
[left, right)左闭右开,所以当nums[mid]被检测之后,下一步的搜索区间应该去掉 mid 分割成两个区间,即[left, mid)或[mid + 1, right)
- 因为我们的「搜索区间」是
-
Q3:为什么该算法能够搜索左侧边界?
- 关键在于对于
nums[mid] == target这种情况的处理:if (nums[mid] == target) right = mid; - 找到 target 时不要立即返回,而是缩小「搜索区间」的上界 right,在区间
[left, mid)中继续搜索,即不断向左收缩,达到锁定左侧边界的目的
- 关键在于对于
-
Q5:能不能令
right = nums.length - 1,也即继续使用两边都闭合的「搜索区间」?- 如果要令
right = nums.length - 1,那么就需要修改 while 语句为:while (left <= right) - 那么更新 left 和 right 的逻辑需要修改
if (nums[mid] == target) { right = mid - 1; // 收缩右侧边界 } else if (nums[mid] < target) { left = mid + 1; // 搜索区间变为 [mid+1, right] } else if (nums[mid] > target) { right = mid - 1; // 搜索区间变为 [left, mid - 1] } - 在该条件下,是通过
left = right + 1来退出循环,注意这个情况退出循环的分析:- 此时
left = right + 1退出循环的时候是没有去判断array[left]是否等于 target 的,所以需要去判断 - 当
target比nums中所有元素都大时,会存在left = right + 1 = nums.length的情况使得索引越界,因此在最后需要添加判断
- 此时
- 完整代码在变形代码 1
- 如果要令
-
变形代码 1
public int leftBound(int[] nums, int target) {
int left = 0;
int right = nums.length - 1; // 注意和上一版本对比
while(left <= right) { // 注意和上一版本对比
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
right = mid - 1; // 注意和上一版本对比
}
else if (nums[mid] < target) {
left = mid + 1;
}
else if (nums[mid] > target) {
right = mid - 1; // 注意和上一版本对比
}
}
if (left == nums.length || nums[left] != target) return -1;
return left;
}
寻找右边界
- 在一个存在重复数字的数组中搜索一个数,如果该数存在则返回最右边的下标,不存在则返回 -1
public int rightBound(int[] nums, int target) {
int left = 0;
int right = nums.length; // 注意和下一版本对比
while(left < right) { // 注意和下一版本对比
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return left = mid + 1; // 注意和下一版本对比
}
else if (nums[mid] < target) {
left = mid + 1; // 注意和下一版本对比
}
else if (nums[mid] > target) {
right = mid // 注意和下一版本对比
}
}
if (left == 0 || nums[left - 1] != target ) return -1;
return left - 1; // 返回值需要 -1
}
-
Q1:为啥这里要写成
right = nums.length使得「搜索区间」变成左闭右开呢?- 上面解答过
-
Q2:为什么 while 中是
<而不是<=?- 上面解答过
-
Q3:为什么该算法能够搜索右侧边界?
- 关键在于对于
nums[mid] == target的处理:if (nums[mid] == target) left = mid + 1; - 当
nums[mid] == target时不要立即返回,而是增大「搜索区间」的下界 left,使得区间不断向右收缩,达到锁定右侧边界的目的
- 关键在于对于
-
Q4:为什么最后返回
left - 1而不像左侧边界的函数,返回left?而且我觉得这里既然是搜索右侧边界,应该返回right才对- 首先,退出循环的条件之一就是
left == right,那么返回right - 1和 返回left - 1是一样的 - 再者,为什么是返回
left - 1而不是left?- 关键点还是在
if (nums[mid] == target) return left = mid + 1;因为相等的时候left是先加了 1 的,那么当返回mid的下标的时候,left当然需要减去 1 再返回:mid = left - 1
- 关键点还是在
- 首先,退出循环的条件之一就是
-
Q5:能不能令
right = nums.length - 1,也即继续使用两边都闭合的「搜索区间」?- 如果要令
right = nums.length - 1,那么就需要修改 while 语句为:while (left <= right) - 那么更新 left 和 right 的逻辑需要修改
if (nums[mid] == target) { left = mid + 1; // 收缩左侧边界 } else if (nums[mid] < target) { left = mid + 1; // 搜索区间变为 [mid + 1, right] } else if (nums[mid] > target) { right = mid - 1; // 搜索区间变为 [left, mid - 1] } - 在该条件下,是通过
left = right + 1来退出循环,注意这个情况退出循环的分析:- 此时
left = right + 1退出循环的时候是没有去判断array[right]是否等于 target 的,所以需要去判断 - 当
target比nums中所有元素都小时,会存在right = left - 1 = -1的情况使得索引越界,因此在最后需要添加判断
- 此时
- 完整代码在变形代码 1
- 如果要令
-
变形代码 1
public int rightBound2(int[] nums, int target) {
int left = 0;
int 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) {
left = mid + 1;
}
else if (nums[mid] > target) {
right = mid - 1; // 注意和上一版本对比
}
}
if (right < 0 || nums[right] != target ) return -1;
return right; // 注意和上一版本对比
}
总结(逻辑统一)
寻找一个数Ⅰ
如果初始化:
right = nums.length
那么 while 循环的条件为:
while (left < right)
那么 left 和 right 的更新逻辑为:
if (nums[mid] == target) return mid;
else if (nums[mid] < target) left = mid + 1;
else if (nums[mid] > target) right = mid;
最后需要先判断再返回:
if (left == nums.length || nums[left] != target) return -1;
return left;
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length;
while(left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
}
else if (nums[mid] < target) {
left = mid + 1;
}
else if (nums[mid] > target) {
right = mid; // 后面会讲为什么是 mid 而不是 mid - 1
}
}
if (left == nums.length || nums[left] != target) return -1;
return left;
}
寻找左边界Ⅰ
如果初始化:
right = nums.length
那么 while 循环的条件为:
while (left < right)
那么 left 和 right 的更新逻辑为:
if (nums[mid] == target) right = mid;
else if (nums[mid] < target) left = mid + 1;
else if (nums[mid] > target) right = mid;
最后需要判断再返回:
if (left == nums.length || nums[left] != target) return -1;
return left;
public int leftBound(int[] nums, int target) {
int left = 0;
int right = nums.length; // 注意和下一版本对比
while(left < right) { // 注意和下一版本对比
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; // 注意和下一版本对比
}
}
if (left == nums.length || nums[left] != target) return -1;
return left;
}
寻找右边界Ⅰ
如果初始化:
right = nums.length
那么 while 循环的条件为:
while (left < right)
那么 left 和 right 的更新逻辑为:
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 || nums[left - 1] != target) return -1;
return left - 1;
public int rightBound(int[] nums, int target) {
int left = 0;
int right = nums.length; // 注意和下一版本对比
while(left < right) { // 注意和下一版本对比
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return left = mid + 1; // 注意和下一版本对比
}
else if (nums[mid] < target) {
left = mid + 1; // 注意和下一版本对比
}
else if (nums[mid] > target) {
right = mid // 注意和下一版本对比
}
}
if (left == 0 || nums[left - 1] != target ) return -1;
return left - 1; // 返回值需要 -1
}
寻找一个数Ⅱ
如果初始化:
right = nums.length - 1
那么 while 循环的条件为:
while (left <= right)
那么 left 和 right 的更新逻辑为:
if (nums[mid] == target) return mid;
else if (nums[mid] < target) left = mid + 1;
else if (nums[mid] > target) right = mid - 1;
最后可以直接返回:
return -1;
public int binarySearch(int[] array, int target) {
int left = 0;
int right = array.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (array[mid] == target) {
return mid;
} else if (array[mid] < target) {
left = mid + 1;
} else if (array[mid] > target) {
right = mid - 1;
}
}
return - 1;
}
寻找左边界Ⅱ
如果初始化:
right = nums.length - 1
那么 while 循环的条件为:
while (left <= right)
那么 left 和 right 的更新逻辑为:
if (nums[mid] == target) right = mid - 1;
else if (nums[mid] < target) left = mid + 1;
else if (nums[mid] > target) right = mid - 1;
最后需要判断再返回:
if (left == nums.length || nums[left] != target) return -1;
return left;
public int leftBound(int[] nums, int target) {
int left = 0;
int right = nums.length - 1; // 注意和上一版本对比
while(left <= right) { // 注意和上一版本对比
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
right = mid - 1; // 注意和上一版本对比
}
else if (nums[mid] < target) {
left = mid + 1;
}
else if (nums[mid] > target) {
right = mid - 1; // 注意和上一版本对比
}
}
if (left == nums.length || nums[left] != target) return -1;
return left;
}
寻找右边界Ⅱ
如果初始化:
right = nums.length - 1
那么 while 循环的条件为:
while (left <= right)
那么 left 和 right 的更新逻辑为:
if (nums[mid] == target) left = mid + 1;
else if (nums[mid] < target) left = mid + 1;
else if (nums[mid] > target) right = mid - 1;
最后需要判断再返回:
if (right < 0 || nums[right] != target) return -1;
return right;
public int rightBound2(int[] nums, int target) {
int left = 0;
int 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) {
left = mid + 1;
}
else if (nums[mid] > target) {
right = mid - 1; // 注意和上一版本对比
}
}
if (right < 0 || nums[right] != target ) return -1;
return right; // 注意和上一版本对比
}