二分查找深度分析
Posted baizihua
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了二分查找深度分析相关的知识,希望对你有一定的参考价值。
总结一句话就是:思路很简单,细节是魔鬼,hhhh。
本博客探究几个最常用的二分查找场景:寻找一个数、寻找左侧边界、寻找右侧边界。
寻找一个数(基本的二分搜索)
public int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while(left <= right) { // 注意点
int mid = (right + left) / 2;
if(nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1;
else if (nums[mid] > target)
right = mid - 1;
}
return -1;
}
- 为什么 while 循环的条件中是 <=,而不是 < ?
答:
举个例子推一下即可得知。因为当left=4,right=6时,此时mid=5;如果进入(nums[5] < target)分支时,left=6,如果是<,下次循环left=right=6不满足left<right,此时会跳出循环返回-1,然而索引是6的元素还没有判断是否等于target,故while 循环的条件中是 <=。
left <= right相当于两端都闭区间 [left, right]中搜索,left < right相当于左闭右开区间 [left, right)中搜索。
- 此算法有什么缺陷?
答:
比如说给你有序数组 nums = [1,2,2,2,3],target = 2,此算法返回的索引是 2,没错。但是如果我想得到 target 的左侧边界,即索引 1,或者我想得到 target 的右侧边界,即索引 3,这样的话此算法是无法处理的。
这样的需求很常见。你也许会说,找到一个 target,然后向左或向右线性搜索不行吗?可以,但是不好,因为这样难以保证二分查找对数级的复杂度了。
我们后续的算法就来讨论这两种二分查找的算法。
寻找左侧边界的二分搜索
public int left_bound(int[] nums, int target) {
if (nums.length == 0) return -1;
int left = 0;
int right = nums.length; // 注意点1
while (left < right) { //注意点2
int mid = (left + right) / 2;
if (nums[mid] >= target) { // 注意点3
right = mid;
} else if (nums[mid] < target) {
left = mid + 1;
}
}
// target 比所有数都大
if (left == nums.length) return -1;
// 类似之前算法的处理方式
return nums[left] == target ? left : -1;
}
- 为什么该算法能够搜索左侧边界?(注意点3)
答:
关键在于对于 nums[mid] == target 这种情况的处理:
if (nums[mid] >= target) { // 注意点
right = mid;
}
即找到 target 时不是立即返回,而是缩小搜索区间的上界 right,在区间 [left, mid) 中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。
为什么不是right = mid -1?因为这有可能会错过想要找的目标,比如:数组中存在唯一的值等于target,当mid刚好等于该值索引,进入(nums[mid] >= target),则right = mid -1;结果是错过唯一的目标,找不到target。
- 为什么这里的while循环条件是left < right而不是<=(注意点2)
举个例子:
int[] nums = {1,2,4,6,6,6,6,6,6,6,6,9};
target = 6;
如果是left <= right,则会出现这种情况:
left=3,right=3,此时mid=3,当target在mid处命中即target=nums[mid],此时进入(nums[mid] >= target)分支,right = 3;所以这种情况它跳不出循环。
为什么int right = nums.length?数组不会越界?(注意点1)
答:
left=9,right=10,(9+10)/2=9;
left=10,right=11,(10+11)/2=10;
所以mid永远不会超过nums.length-1那为什么和第一种情况中right = nums.length-1不一样呢?
因为left <= right代表[left,right],而left < right代表[left,right)为什么最终返回left而不是right?
答:
都是一样的,因为 while 终止的条件是 left == right。
寻找右侧边界的二分查找
int right_bound(int[] nums, int target) {
if (nums.length == 0) return -1;
int left = 0, 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,2,3],left=0,right=2,mid=1,进入左分支需要比较2次,进入右分支需要比较3次。
解决办法:
1,斐波那契数(Fibonacci)
2,变为2次比较
如有时间,下次阐述原理。
以上是关于二分查找深度分析的主要内容,如果未能解决你的问题,请参考以下文章