二分查找
文章目录
二分查找的想法很简单,但是实现起来非常的困难,就是"思路很简单,细节是魔鬼",有很多人喜欢使用哪整型的溢出来说二分查找很困难,但是二分查找的困难真正的"坑"还是在,到底要不要给mid加一或者减一,while里面到底是使用<=还是<
你如果没有理解mid和while,那你到现在的二分查找写的题,都是玄学编程
基本的二分查找
这个场景是最简单的,就是搜索一个数字,如果存在返回它的索引,反之,返回-1
int BisicSearvh(int[] nums, int target){
int left = 0;
int right = nums.length - 1;//注意
while (left <= right) {
//注意2
int mid = (right - left) / 2 + left;//注意
if(nums[mid] == target){
return mid;
}else if(nums[mid] < target){
left = mid + 1; //注意
}else if(nums[mid] > target){
right = mid - 1;//注意
}
}
return -1;
}
1.计算mid的时候为什么不能使用(left + right) / 2?
因为直接相加可能会导致整型溢出
注意:为什么while循环的条件是<=而不是=(非常重要!!!)
因为初始化的时候是[0 , num.length-1],这相当于两端都是闭区间,如果使用的是<就相当于[0,num.length)这时我们就会少计算一个值(如果你想问为什么不能[0,num.length)?继续向下看吧,我在后面就会讲到)
那什么时候停止循环呢?当然是找到目标的时候就退出循环
if(nums[mid] == target)
return mid;
但是如果没有找到?也就是while循环到底要什么时候终止?搜索区间没有东西了,就意味着你没得找了,就等于没找到
while (left <= right)
这个的终止条件就是left == right + 1 ,写成区间的形式就是[right+1, right]这个时候while循环终止就是正确的,直接返回-1就可以了
那么下面的终止条件就是?
while (left < right)
没错就是left = right,这个时候循环就终止了,你有没有意识到,我们漏掉了一个数字?对,就是最后面的数字漏掉了,所以我们在最后打一个补丁就好了,这个补丁就自己想
2.为什么是left = mid + 1, right = mid -1?什么时候加一,什么时候减一,什么时候不加不减,这个要怎么判断?
这个也是二分查找的一个难点,你只要能理解前面的内容,就能够很容易的判断
刚刚咱们的算法是闭区间的,当我们发现target不是我们要找的值时,我们下一步应该要去哪里进行搜索呢?
当然是[left ,mid - 1]或者[mid + 1, right],因为mid我们已经搜索过了,应该从搜索的区间中去除
3.这个算法有什么缺陷?
比如说给你一个数组[1, 2, 2, 2, 3],target = 2,这个算法返回的索引是2,那我们如果想要找到2,第一次出现的位置,或者最后出现的一个位置应该怎么办?最简单的方法就是我们使用线性搜索,但是如果我们使用的线性搜索我们就违背了我们使用二分查找的作用.因为使用线性搜索难以保证二分查找搜索的时间复杂度
那么我们就开始来讨论另外的两个二分查找的算法
寻找左侧边界的二分搜索
这个就是最基本的寻找左侧边界的闭区间二分搜索,开区间后面会讲
int Left_Searvh(int[] nums, int target){
int left = 0;
int right = nums.length - 1;
while (left <= right) {
//注意
int mid = (right - left) / 2 + left;
if(nums[mid] == target){
right = mid - 1;//注意
}else if(nums[mid] < target){
left = mid + 1; //注意
}else if(nums[mid] > target){
right = mid - 1;//注意
}
}
return left;
}
1.为什么没有返回-1的操作,如果num不存在target,该怎么办?
我们先来理解一下这个左侧边间有什么,特殊的含义?
对于数组[1, 2, 2, 2, 3],target = 2,我们可以这样解读,数组中小于2,的元素有几个.
所以我们可以看出函数的返回值(left的值,即小于target的值有几个),在[0, num.length]
因为搜索区间都是闭区间,并且现在是搜索左边界,所以while的退出条件是left == right + 1,所以 target比数组中所有的元素都要大的时候,就会出现left越界,所以我们要在最后的代码中检查越界的情况
while (left <= right) {
//.........
}
if(left >= nums.length || nums[left] != target)
return -1;
return left;
2.为什么这个算法可以去搜索到左侧的边界
关键的答案就在
right = mid - 1
这个我们可以理解,在找到目标值的时候,我们不用立刻返回,而是缩小[搜索区间]的上界right,在区间[left, mid-1],不断的向左收缩,达到锁定左侧边界的目的
3.这种写法只是为了和第一种写法统一,对于左右边界的二分查找,使用左闭右开更加的普遍,我们就来写写把[搜索区间]变成左闭右开的形式练练我们的搜索区间吧
int Left_Searvh(int[] nums, int target){
int left = 0;
int right = nums.length;
while (left < right) {
int mid = (right - left) / 2 + left;
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;
}
为什么是left = mid + 1, right = mid?这个和之前的算法有什么不同吗?
这个很好解释,我们的[搜索区间] 是 [left, right)左闭右开, 所以当nums[mid] 被检测之后,下一步的搜索区间应该去掉mid分割成两个区间,即[left, mid) 或者 [mid + 1, right)
寻找右侧边界的二分搜索
这时我们应该已经把左侧边界的二分查找掌握了,我们就来使用右侧边界二分查找来练练手,我把闭区间和左闭右开区间的代码给你(仅做参考,还是要自己敲的)
1.闭区间
int Right_Searvh(int[] nums, int target){
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int mid = (right - left) / 2 + left;
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;
}
2.左闭右开
int Right_Searvh_1(int[] nums, int target){
int left = 0;
int right = nums.length;
while (left < right) {
int mid = (right - left) / 2 + left;
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;
}
小结
1.分析二分查找的时候我们最好不要出现else,全部展开为else if的形式方便理解
2.注意[搜索区间]和while的终止条件,如果存在漏电的元素,记得在最后面进行检查