本来想着在原来的文章的基础上进行改进,但是这样原来的文章就会内容太复杂了,原文可参考:Algorithm-Binary Search算法。所以单独写一篇文章来说明我都二分搜索的进一步理解。
下面的例子中的nums都是不下降的数列,不上升的数列其实就是不下降的数列反过来嘛,我就不罗嗦了。
下面我将从两个简单的例子来说明二分搜索不同类型问题中基本的共性之处。
2018-5-12在LeetCode上做到二分搜索的变形题目1,2,3;
2018-5-16更新题目4。
首先,可以来尝试解决两个最基本的问题:
在我目前的框架下,二分搜索算法的模板第一句是考虑闭区间[left, right]的,所以有:
int mid, left = 0, right = nums.size()-1;
然后,循环条件设定为:
while(left < right){}
这里也可以设定为 <= 或者 >=,但这是进一步的思考了,所以我希望可以先用最简单的形式来思考最简单的问题。
首先来思考一下第一个问题,要找最小的i使得:nums[i] = target,那么举个例子:
[1,2,3,3,3,3,3,5,5,5,6,8,8,8,8,9,9,9,9]
target=3,那么有很多个3,我们就要找到第一个出现的3.如果是要解决问题2,那么就应该找到最后一个出现的3。
那么,现在有很多个3,我们要么要找第一个出现的,要么要找最后一个出现的,根据二分查找的思想,如果mid刚好是第一个或者最后一个3,那么就万事大吉。但是很多时候可能刚刚好:nums[mid] = 3,但是,这个3不是第一个出现也不是最后一个,而是中间的3。这个时候,就需要一些方法来解决这个问题。
回顾一下二分查找的方法,主体算法是:
根据以上主体算法流程,再结合我们之前思考的问题,我们应该解决步骤5的情况。那就是,当nums[mid] = target,应该怎么做。
要找到最小的i,nums[i] = target,那么我们应该尽可能往小的找,而当nums[mid]等于target的时候,我们还是希望下一个mid能出现在当前mid与left之间的位置的某一处。
这样,我们就能明白:要找到最小的i,那么当nums[mid] = target时,我们应该更新right,这样,下一个mid就会出现在left与当前mid之间了。所以,我们就可以得到二分搜索中,while中循环判断部分代码:
if(nums[mid] < target ) left = mid + 1;
else right = mid;
这里的else 就是当nums[mid] >= target的时候,很明显,包含nums[mid] = target 的情况。
这里我们还要思考,为啥left更新为mid+1,right更新为mid呢?
那么,我们进一步可以解决主体算法流程中的第8步,循环结束后,搜索结果在哪里?只可能在right取到!因为所有的nums[left] < target, 而 nums[right] >= target啊。所以 nums[right] 有可能等于target。也有可能不等,不等就意味着没有找到要搜索的结果。
所以有返回代码:
return nums[right] == target ? right : -1;
那么现在还只剩下一个问题,left与right的更新方式都解决了,那就只剩下mid的了。mid是left与right 的中间值,但是这是数列,mid是向上取整呢还是向下取整呢?我理解的方式是看下面这个例子:
四个元素:left,mid1,mid2,right
那么很明显,当mid向下取整时,对应left与right的“中间值”是mid1;反过来,mid向上取整时,“中间值”是mid2。如果nums[mid1] = nums[mid2],那么根据现在考虑的问题:要找到最小的i,使得nums[i] = target, 我想自然就应该是mid向下取整更加合适。那么对应,如果是要找到最大的i,使得nums[i] = target,就应该是向上取整 了。
所以对于这个问题,可以有:
注意这里为了防止int型溢出,采用这样计算,此外运算符+优先级高于 > > ,所以要有两层括号,可参考:C++——运算符优先级。
//向下取整
mid = left + ((right-left)>>1);
//向上取整
mid = left + ((right-left + 1)>>1);
那么,至此,我们就可以整理一下之前一大段分析,从而得到解决问题:找到最小的i,使得nums[i] = target的二分搜索的代码了:
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int mid, left=0, right=nums.size()-1;
while(left < right){
mid = left + ((right-left)>>1);
if(nums[mid] < target) left = mid + 1;
else right = mid;
}
return nums[right] == target ? right : -1;
}
};
现在我们可以更快的按照问题一的思路来解决问题二:找到最大的i,使得nums[i]=target。
根据以上分析,就不难得出代码:
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int mid, left=0, right=nums.size()-1;
while(left < right){
mid = left + ((right-left+1)>>1);
if(nums[mid] > target) right = mid - 1;
else left = mid;
}
return nums[left] == target ? left : -1;
}
};
所以结合这两个基本问题,不难发现注意好以上6个步骤就可以解决基本的二分搜索的问题了。
以上就是两种最简单的问题的分析思路了。我是分析由繁琐到简洁,但是,必须多做几种不同类型的题目,才能好好体会不同二分搜索之中的相同点与不同点。
一样的思路,我们来逐一思考:
所以,代码自然而然就可以改写如下:
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int mid, left=0, right=nums.size()-1;
while(left < right){
mid = left + ((right-left)>>1);
if(nums[mid] > target) right = mid;
else left = mid + 1;
}
return nums[right] > target ? right : -1;
}
};
这里需要说明的时,这个问题与前面两个问题不同的就是,“要求条件”从相等变成小于,所以,nums[right] > target 的时候,有可能这里的right就是我们要找的值,也有可能不是,所以right可以更新,但是不能更新为mid-1,只能更新为mid。那么对应的left就应该更新为 mid + 1。
同样,结果返回值处,也应该思考要求条件满足就是nums[right] > target的时候,满足那么right就是要找到值。
一样的思路,我们来逐一思考:
代码如下:
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int mid, left=0, right=nums.size()-1;
while(left < right){
mid = left + ((right-left+1)>>1);
if(nums[mid] < target) left = mid;
else right = mid - 1;
}
return nums[left] < target ? left : -1;
}
};
要注意,由要更新的是left或right反推if条件的时候要注意,由于我们是反推,所以一定要注意这里的逻辑关系。
要更新left --> nums[mid] < target --> left = mid
要更新right --> nums[mid] > target --> right = mid
其实,最后可以和“要求条件”对照一下,因为返回值 return 的时候比较的都是 = mid的时候,因为 mid + 1或者 mid - 1都被跳过了。
要求条件还是大于,只是要找最大的i,那么同样的,我们有:
同样的,代码如下所示:
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int mid, left=0, right=nums.size()-1;
while(left < right){
mid = left + ((right-left+1)>>1);
if(nums[mid] < target) left = mid;
else right = mid - 1;
}
return nums[left] > target ? left : -1;
}
};
测试代码,发现???有问题?仔细看看问题,对于不下降数列,求最大的i,使得nums[i] > target?,这需要二分搜索求么?不直接返回nums.size()-1不就好了么?而且仔细看一看,就会发现之前的思路中有一个地方有Bug,那就是4与5.
4要求nums[mid] > target的时候,更新left,但是5又说,由于更新left,所以应该是nums[mid] < target的时候,这不就矛盾了么,所以这个问题是有问题的!
同样的,这个问题我也是在一本正经的胡说八道。
要求条件此时变成小于,要找最小的i,同样的有:
所以代码如下:
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int mid, left=0, right=nums.size()-1;
while(left < right){
mid = left + ((right-left)>>1);
if(nums[mid] < target) right = mid;
else left = mid + 1;
}
return nums[right] < target ? right : -1;
}
};
这一篇博客我费了九牛二虎之力,用了三寸不烂之舌,把我自己对二分搜索的理解应用在了四个最基本的问题上。书从薄到厚,再从厚到薄。最后总结一下,二分搜索主要就是要注意:
这个东西基础特别重要,所以这篇博客我一定要经常搞不清楚就来多看看。此外,这篇博客主要是我自己的理解,那么之前的博客可以多看看其他代码,毕竟万变不离其宗。
多版本二分搜索代码解答参考:Algorithm-Binary Search算法。