2020-09-02

二分查找那些坑

1. 二分搜索:寻找一个数

如下两种实现方式,right 边界的取值,直接影响着算法的实现细节,这些细节稍不小心就会犯错。二分查找看似简单,实则不然,真正能考虑到所有细节,把代码写准确是很不容易的。

int binarySearch(int[] nums, int target) {
    int left = 0; 
    int right = nums.length - 1; // 注意,搜索区间是[left, right],左闭右闭

    while(left <= right) {// 注意,此处是<=, 因为搜索区间左闭右闭,left==right也应该考虑进来
        int mid = (right + left) / 2;
        if(nums[mid] == target)
            return mid; 
        else if (nums[mid] < target)
            left = mid + 1; // 注意,搜索区间是左闭右闭,mid已经被搜索过了,此处直接mid+1
        else if (nums[mid] > target)
            right = mid - 1; // 注意,搜索区间是左闭右闭,mid已经被搜索过了,此处直接mid-1
        }
    return -1;
}
int binarySearch(int[] nums, int target) {
    int left = 0; 
    int right = nums.length; // 注意,搜索区间是[left, right),左闭右开

    while(left < right) {// 注意,此处是<, 因为搜索区间左闭右开,left==right不应该考虑
        int mid = (right + left) / 2;
        if(nums[mid] == target)
            return mid; 
        else if (nums[mid] < target)
            left = mid + 1; // 注意,搜索区间是左闭右开,left边界会被搜索,此处left=mid+1
        else if (nums[mid] > target)
            right = mid; // 注意,搜索区间是左闭右开,此处不能令right = mid - 1,因为right边界不会被搜索,mid-1会被missed掉,所以必须是right = mid。
        }
    return -1;
}

1.  while 循环的条件中 <=,< 有什么区别?
答:如果初始化 right 的赋值是 nums.length-1,即最后一个元素的索引,相当于两端都闭区间 [left, right],则 while 循环的条件中 用<=如果初始化 right 是 nums.length,相当于左闭右开区间 [left, right),则 while 循环的条件中 <,因为索引大小nums.length 是越界的。

2. 为什么第一种算法是 left = mid + 1,right = mid - 1,第二种算法是 left = mid + 1,right = mid ,如何判断?
答:这也是二分查找的一个难点,不过只要你能理解前面的内容,就能够很容易判断。刚才明确了「搜索区间」这个概念,如果算法的搜索区间是两端都闭的,即 [left, right]。那么当我们发现索引 mid 不是要找的 target 时,如何确定下一步的搜索区间呢?
当然是 [left, mid - 1] 或者 [mid + 1, right] ,因为 mid 已经搜索过,应该从搜索区间中去除。相应的,如果算法的搜索区间是左闭右开的,即 [left, right),则当mid 不是要找的 target 时,下一步的搜索区间是 [left, mid) 或者 [mid + 1, right)。

2. 二分搜索:寻找一个数的左侧边界

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; // 注意 不是mid - 1
        }
    }

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

1. 为什么该算法能够搜索左侧边界?
答:关键在于对于 nums[mid] == target 这种情况的处理:
if (nums[mid] == target) {

     right = mid;

}
可见,找到 target 时不要立即返回,而是缩小「搜索区间」的上界 right,在区间 [left, mid)中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。另外,为什么返回 right 而不是 left?其实返回哪个都一样,因为 while 终止的条件是 left == right。

3. 二分搜索:寻找一个数的右侧边界

int right_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) {
            left = mid + 1; // 注意
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid; // 注意
        }
    }

    if (left == 0) return -1;
    return nums[left-1] == target ? (left-1) : -1;
}

1. 为什么这个算法能够找到右侧边界?
答:类似地,关键点还是这里:

if (nums[mid] == target) {

    left = mid + 1;

}

当 nums[mid] == target 时,不要立即返回,而是增大「搜索区间」的下界 left,使得区间不断向右收缩,达到锁定右侧边界的目的。另外,为什么最后返回 left - 1 ?while 循环的终止条件是 left == right,所以返回 left -1 或 right - 1是一样的。由于区间向右收缩的过程中是 left = mid + 1 赋值,所以循环终止的时候肯定 nums[left] != target, 而nums[left - 1] 才可能是右侧边界。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值