二分法易错点分析,结合leetcode162题

二分法解析

参考:二分查找算法详解

二分法是一个看似简单实则细节中暗藏魔鬼的方法。避免二分法出错的关键是,把握住搜索区间

一、基本的二分法

常规的二分法套路如下:


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;
}

1.为什么 while 循环的条件中是 <=,而不是 < ?
在二分法中,通常right的赋值是nums.length - 1,即搜索空间是闭区间(左右两端都可以遍历到)。
对于right=nums.length 的情况,我们称之为开区间。因为nums.length无法被索引的。二分法的终止条件可以看做是搜索区间为空
例如:while(left <= right)的终止条件是 left == right + 1,写成区间的形式就是 [right + 1, right],或者带个具体的数字进去 [3, 2],可见这时候搜索区间为空,
若是,while(left < right)的终止条件是 left == right,写成区间的形式就是 [right, right],或者带个具体的数字进去 [2, 2],这时候搜索区间非空。此时就应当另外判别索引为2时的值,是否等于target,是就返回left,否就返回-1。

那么我们来分析一下,如果我们设置①的条件为right =nums.length,②的条件为left <= right。假定nums=[7,8,9,10],target=11。显然,最终mid=4,搜索范围超过实际长度。设置①的条件为right =nums.length-1,②的条件为left < right,最终left=right=3时,跳出,无法索引到3。
2.为什么 left = mid + 1,right = mid - 1?
因为mid被索引过后,就不需要再次被索引,对于初始区间闭区间的情况,只有将去[left, mid - 1] 或者 [mid + 1, right] 去搜索才能彻底避开mid。

二、寻找左侧边界的二分法

标记的是需要注意的细节


int left_bound(int[] nums, int target) 
{    
    if (nums.length == 0) 
        return -1;    
    int left = 0;    
    int right = nums.length; // 注意    
    while (left < right) { // 注意        
        int mid = (left + right) / 2;        
        if (nums[mid] == target) {            
            right = mid;       
        } else if (nums[mid] < target) {            
            left = mid + 1;        
        } else if (nums[mid] > target) {            
            right = mid; // 注意        
        }    
    }   
return left;
}

1.是while (left < right)?为何不是while (left <= right)
把握住搜索区间。当前搜索区间为左闭右开区间[left,rjght),终止条件为left==right,所以当达到终止条件时,[left,right),搜索区间为空。
2.为何left = mid + 1,right = mid ?和之前的算法不一样?
因为搜索区间[left,right)是左闭右开。当mid被索引过后,初始区间为左闭右开,所以应该将区间分割为[left, mid) 或 [mid + 1, right)两个左闭右开区间。对比(一)中,mid被检测后,分成两个闭合区间。
3.为何该算法可以搜索左侧边界?
关键在于对于 nums[mid] == target 这种情况的处理:

if (nums[mid] == target)        
    right = mid;

可见,找到 target 时不要立即返回,而是缩小「搜索区间」的上界 right,在区间 [left, mid) 中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。
4.为什么没有返回 -1 的操作?如果 nums 中不存在 target 这个值,怎么办?
可以注意到,mid的搜索范围在[0, nums.length],所以我们简单添加两行代码就能在正确的时候 return -1;


while (left < right) {    
    //...}
// target 比所有数都大
if (left == nums.length) 
    return -1;
// 类似之前算法的处理方式
return nums[left] == target ? left : -1;

三、寻找右侧边界的二分法



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;        
        }   
    }   
    return left - 1; // 注意
}

1.能找到右侧边界的原因。

if (nums[mid] == target) {       
    left = mid + 1;

当 nums[mid] == target 时,不要立即返回,而是增大「搜索区间」的下界 left,使得区间不断向右收缩,达到锁定右侧边界的目的。
2.为什么最后返回 left - 1
可以注意到根据迭代终止条件,最终结果为left = right,所以返回结果亦可是right-1。那为何要减一呢?因为left更新时必然是mid+1,也即我们终止条件时获得的left,满足条件的mid+1的值,为了得到满足条件的mid,需要减一。

下面以leetcode162题为例,说一说二分法的应用。

在这里插入图片描述


class Solution {
    public int findPeakElement(int[] nums) {
        if(nums.length<=1)
            return 0;
        int left=1,right=nums.length;
        //int left=0,right=nums.length-1;
        for(;left<right;){
            int mid=(right+left)/2;
            
            if(nums[mid-1]<nums[mid])
                left=mid+1;//①
            else{
                right=mid;//②
            }
            //下面被注释部分是leetcode常见的题解
            // if(nums[mid+1]<nums[mid])
            //     right=mid; ③
            // else{
            //     left=mid+1; ④
            // }
        }
        return left-1;
        //return left;
    }
}

可以看到,不注释的部分是搜索左边界。注释部分是搜索右边界。
下面对两部分代码做解释。
首先是不注释部分:
1.为何最后return left-1?
需要注意的是,当最后一次循环满足条件①时,mid是最大值,而left=mid+1,所以要返回left-1。同理,当满足条件二时,mid-1是最大值,left=right=mid-1,所以要返回left-1。
2.为何left设为1,而right设为nums.length?
我们可以做一个假设,如果峰值在最左侧,即索引为0的位置。当left设为0时,显然会出现mid=0的情况,此时mid-1的索引无效。这也就是为何原来会有判定mid为0的代码。那如果峰值在最右侧,则最终状态时,left=right=num.length,返回值为left-1,即数组的最后一个值,正确。

然后是注释部分:
1.为何返回return left?
首先,循环条件终止时,left=right,你想返回right亦可。分析过程与上面的第一个问题类似,若峰值在最右侧,循环终止前的最后一次迭代,峰值在mid+1处,left=mid+1,返回left,没错。若峰值在最左侧,同理分析。
2.为何left设为0,而right设为nums.length-1?
分析方式与上面第二个问题一样。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值