个人笔记比较流水账,主要记录自己的思考过程哈
二分查找
leetcode题目链接:https://leetcode-cn.com/problems/binary-search/
第一种解法(也是最常见的解法):但只针对当前题目
- 左闭右闭:right=nums.lenght-1;
- 左闭右开:right=nums.lenght;
上面右边界的开或关决定了while循环中的等号是否支持,和right=middle+1;还是right=middle。边界值很重要(关键是要保证能遍历到数组中的每一个值与nums[middle]进行比对)
public int search(int[] nums, int target) {
int left=0;
//区间左闭右闭方法 []
//int right=nums.length-1;
// while(left<=right){
// int middle=left+(right-left)/2;
// if (nums[middle]<target){
// left=middle+1;
// }else if(nums[middle]>target){
// right=middle-1;
// }else{
// return middle;
// }
// }
// return -1;
//区间左闭右开方法 [ )
int right=nums.length;
while(left<right){
int middle=left+(right-left)/2;
if (nums[middle]<target){
left=middle+1;
}else if(nums[middle]>target){
right=middle;
}else{
return middle;
}
}
return -1;
}
第二方法:算法摘自UP主《五点七边》链接如下:
https://www.bilibili.com/video/BV1d54y1q7k7?spm_id_from=333.999.0.0
先附上具体算法和不同的问题场景:
最初本人去调的时候也是觉得逻辑很简单,但具体实现的时候就是有部分用例不通过。无奈来B站找找好的老师。最先接触到的是第一种左右边界的方法,理解起来就有点晕晕的。后来又看到了五点七边老师的视频。刚开始看的时候还带有右边界是开还是关的思维,就半天没理解五点七边的逻辑。后来看了两遍自己拿个例子推了一遍,感觉自己明白了,哈哈哈。
为啥要划分蓝红两个区域呢,通过最后视频中给出的例子的答案可以反推出来。我们的最终目的是要依据问题场景确定返回 left 还是right 。看完觉得我看着已经知道全部数组元素的成员还是蛮简单找出蓝红边界的条件的。可现实是我不知道数组长啥样。嗯!再观察一下例子我们就可以知道了。
注意五点七边的方法是我们要找的target一定是存在于目标数组中的。具体使用时我们要根据具体问题做些许的调整。如下面的栗子所示:
第一个栗子:以本力扣题为例举个栗子:
假如target为X。题目中已说明元素不会重复,且target不一定在数组中。
那么我们的isBlue条件可以确定为第一个小于target的元素,即nums[middle]<target; 退出while循环返回的值为right。那么nums[right]要么等于target,要么大于target。我们再判断一下nums[right]是否对等于target,如果等于就返回right,否则就返回-1.
但是代码中有一个点需要注意(也是提交代码时候才发现):当数组中所有的元素为蓝色区域时。退出while循环返回的right值为N,而数组元素的下标最大值为N-1;此时如果判断nums[right]target时,就会报告数组越界。解决办法是加一个判断,如代码中所示:if(rightnums.length||nums[right]!=target)。
public int search(int[] nums, int target) {
// 五点七边的方法
int left =-1;
int right =nums.length;
while(left+1!=right){
int middle=left+((right-left)/2);
if (nums[middle]<target){
left=middle;
}else{right=middle;}
}
if(right==nums.length||nums[right]!=target){
return -1;
}else{
return right;
}
}
第二个栗子:力扣第278题(https://leetcode-cn.com/problems/first-bad-version/)
题目分析:
//为了便于套用五点七边的方法,我们先把n个版本看成一个数组,bad一定存在于数组中,数组元素为[1,2,3,…,n-1,n],下标依次为[0,1,2,…,n-1],
//但是并不是真正的数组,可以把下标和数组元素看成一样的值,即下标也为[1,2,3,…,n-1,n]
//那么此时的left=0;right=n;isBlue的条件就是蓝色区域全部都是好的版本,红色区域全部都是坏的版本,即 isBadVersion(middle)->false
//isBlue: 最后一个不是bad的版本,返回right
提交的第一个版本如下所示:
/* The isBadVersion API is defined in the parent class VersionControl.
boolean isBadVersion(int version); */
public class Solution extends VersionControl {
public int firstBadVersion(int n) {
//题目分析,bad一定存在于数组中,即存在全部为红色区域的情况,但不存在全部为蓝色区域的情况。数组元素为[1,2,3,...,n-1,n],下标依次为[0,1,2,...,n-1],
//但是并不是真正的数组,可以把下标和数组元素看成一样的值,即下标也为[1,2,3,...,n-1,n]
//那么此时的left=0;right=n;nums[middle]<target就是isBadVersion(middle)->false
//isBlue: 最后一个不是bad的版本,返回right
int left=0;
int right =n;
while(left+1!=right){
int middle=left+((right-left)>>1);
if(isBadVersion(middle)==false){
left=middle;
}else{
right=middle;
}
}
return right;
}
}
提交的结果为:
用例的输入为:
2147483647
2147483647
时跑不过,通过最后的定位呢发现这个是int类型的正数的最大值,当代码执行到最后一次循环的时候(left+1!=right),left+1会发生溢出,所以导致程序发生异常。那么如何解决该问题呢?
我们可以发现(left+1 != right)与(left != right-1)的效果是一样的。所以我们在这里稍作修改就可以了。
所以我们可以把五点七边的方法中的循环条件优化一下,嘻嘻嘻~
/* The isBadVersion API is defined in the parent class VersionControl.
boolean isBadVersion(int version); */
public class Solution extends VersionControl {
public int firstBadVersion(int n) {
//题目分析,bad一定存在于数组中,数组元素为[1,2,3,...,n-1,n],下标依次为[0,1,2,...,n-1],
//但是并不是真正的数组,可以把下标和数组元素看成一样的值,即下标也为[1,2,3,...,n-1,n]
//那么此时的left=0;right=n;nums[middle]<target就是isBadVersion(middle)->false
//isBlue: 最后一个不是bad的版本,返回right
int left=0;
int right =n;
while(left!=right-1){
int middle=left+((right-left)>>1);
if(isBadVersion(middle)==false){
left=middle;
}else{
right=middle;
}
}
return right;
}
}
在来回顾一下上面两个题目,主要是力扣通过之后我在想为啥第二题中的right=n,而不是n+1.
第一题:在数组中找target,target可能存在于数组中,也可能不存在。
蓝红区域分布情况 | |||
---|---|---|---|
存在target | 全部为红色区域 | 蓝色区域+红色区域 | |
不存在target | 全部为蓝色区域 |
第二题:找第一个bad版本,bad版本是一定存在于n个版本中的,所以其蓝红区域分布情况如下:
蓝红区域分布情况 | |||
---|---|---|---|
存在target | 全部为红色区域 | 蓝色区域+红色区域 |
这样也就解释了为啥第二题的right=n,而不是n+1.因为left的最大值是n-1。