二分算法是一种分而治之的思想,这个算法的思想是挺简单的,但是细节部分,比如二分的边界问题十分让人头疼,在处理具体问题时候并不是单纯套一下模板。
正如KMP算法发明者说的一句话:
Although the basic idea of binary search is comparatively straightforward, the details can be surprisingly tricky...
这句话的理解就是:思路很简单,细节是魔鬼。
算法介绍
下面为百度百科的介绍,写的很严谨:
在计算机科学中,二分搜索(binary search),也成为折半搜索(half-interval search)、对数搜索(logarithmic search),是一种在有序数组中查找某一个特定元素的搜索算法。搜索过程从数组的 中间元素开始,如果中间元素正好是查找的元素,则搜索过程结束;如果某一个特征元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样中间开始比较。如果在某一个步骤为空,则代表找不到。这种算法每一次比较都使搜索范围缩小一半。
二分查找两大基本原则
- 每次都要缩减区域
- 每次缩减都不能排除潜在答案
二分查找基本框架
class Solution
{
int binarySearch(int []arr,int target)
{
int low=0,right=arr.length()-1;
while(...)
{
if(arr[mid]==target)
return mid;
else if(arr[mid]>target)
high=...;
else if(arr[mid]<target)
low=...;
}
return -1;
}
};
Tips:上面mid=left+(right-left)/2写成这种形式,不是mid=(right+left)/2,是因为后面一种写法当right和left很大的时候会导致溢出,从而数组访问出现越界的问题。
寻找一个数(基础版本)
class Solution
{
int binarySearch(int []arr,int target)
{
int low=0,right=arr.length()-1;
while(low<=right)
{
if(arr[mid]==target)
return mid;
else if(arr[mid]>target)
high=mid-1;
else if(arr[mid]<target)
low=mid+1;
}
return -1;
}
};
}
为什么while循环的条件是<=,而不是<?
因为初始化right是arr.length-1,即最后一个元素的索引,而不是arr.length。这两者可能出现在不同功能的二分查找中,区别是:前者相当于两端都闭区间[left,right],后者相当于左闭右开区间[left,right)。上面的算法使用[left,right]两端都闭的区间,也建议使用这种区间,不容易疏忽造成越界,这个区间是每次进行搜索的区间,我们不妨称这为[搜索区间]。
当找到目标值的时候会停止搜索
if(arr[mid]==target)
return mid;
或者当搜索区间不存在的时候,也会停止搜索,就是找不到目标。
while(left<=right)
终止条件是left==right+1,写成区间的形式就是[right+1,right],或者带一个数字进去[3,2],这个搜索区间为空,是没有这样的搜索空间的。
while(left<right)
终止条件是left==right,写成区间就是[right,right],带个数字进去就是[2,2],这个时候搜索空间非空,还有一个数2就结束了,但是此时循环语句结束了,也就是区间[2,2]被漏掉,就会造成程序返回值可能就会出现错误。
当然使用while(left<right)也是可以的,只需要
right=mid-1;====>right=mid;//即可
为什么left=mid+1,right=mid-1?有的代码写的是right=mid或者left=mid。
这是二分查找的一个难点,因为我们使用的区间是两端闭区间,即[left,right],当我们发现mid不是要找的target时,如何确定下一步搜索区间?当然应该取[left,mid-1]或者[left+1,right]去找,mid已被搜索过,应该去掉。
理解了上面的内容,大概就了解了二分算法的基础知识了,接下来,我们来探讨一下如何来获取我们获取目标值target第一次出现的位置(即左端点):
第一种写法,比较简单,也比较好理解,直接上代码:
public int binaySearchLeftBound(int[] nums,int target){
int low=0,high=nums.length-1,mid=0;
while(low<=high){
mid=low+(high-low)/2;
if(nums[mid]==target){
if(mid==0||nums[mid-1]<target)//如果mid此时为0,或nums[mid-1]<target,那么mid已经处于左端点处,直接返回
return mid;
high=mid-1;//不断向左边界压
}
else if(nums[mid]>target){
high=mid-1;
}
else if(nums[mid]<target){
low=mid+1;
}
}
return -1;//找不到target
}
第二种写法没有第一种写法那么直白,不过也不难理解,上代码:
public int binaySearchLeftBound(int[] nums,int target){
int low=0,high=nums.length-1;
while(low<=high){
int mid=low+(high-low)/2;
if (nums[mid]==target){
high=mid-1;//不断逼近左边界
}
else if(nums[mid]>target){
high=mid-1;
}
else if(nums[mid]<target){
low=mid+1;
}
}
//上述过程和第一种解法的区别在于,这种写法不需要内部做多次判断,只需要在结尾处判断一次即可
if(low>=nums.length||nums[low]!=target)//left是否都越界了都没找到
return -1;
return low;
}
寻找目标值最后一次出现的问题,即右端点,和寻找左端点思路是相同的
public int binarySearchRightBound(int[] nums,int target){
int low=0,high=nums.length-1;
while(low<=high){
int mid=low+(high-low)/2;
if (nums[mid]==target){
if(mid==nums.length-1||nums[mid+1]>target)
return mid;
low=mid+1;
}
else if(nums[mid]>target){
high=mid-1;
}
else if(nums[mid]<target){
low=mid+1;
}
}
return -1;
}
public int binarySearchRightBound(int[] nums,int target){
int low=0,high=nums.length-1;
while(low<=high){
int mid=low+(high-low)/2;
if (nums[mid]==target){
low=mid+1;
}
else if(nums[mid]>target){
high=mid-1;
}
else if(nums[mid]<target){
low=mid+1;
}
}
if(high<0||nums[high]!=target)
return -1;
return high;
}
二分算法的入门算是讲完了,深入下去还有太多精彩的地方,这个慢慢学习,遇到困难,解决困难!
文章内容参考了一下博客:
https://www.zhihu.com/question/36132386/answer/712269942