二分查找边界问题总结

二分查找很常见,思路也很清晰。
但是还是那句经典的话: 思路越简单,细节越丰富

二分查找 "恶心"人的地方有三个:

  1. while 里面是left <= right 还是 left < right ?
  2. 缩小区间的时候,left 和 right 要不要带上mid?(最可能引起死循环的原因)
  3. 返回啥 ? left 还是 right ?

有时候凭感觉写的对了,有时候就错了,这就是传说中的7分天注定,3分靠感觉!
coding的最终奥义,无脑编程!

拿最常用的三个 二分查找场景

  1. 寻找一个数
  2. 寻找一个数的最左侧出现的地方(左侧边界)
  3. 寻找一个数的最左侧出现的地方(右侧边界)

这三个场景其实可以抽象成两个:

  1. 找到这个数就返回
  2. 找到这个数不返回,继续往左/右找

废话不说,先上菜!
(我把所有可能产生疑问的地方标注出来单一在后面解释。)

case.1

int binary_search(int[] nums, int target) {
    
    
    int left = 0, right = nums.length - 1; 
    while(left <= right) {
    
     
        int mid = ((right - left) >> 1) + left;
        if (nums[mid] < target) {
    
    
            left = mid + 1;
        } else if (nums[mid] > target) {
    
    
            right = mid - 1; 
        } else if(nums[mid] == target) {
    
    
            return mid; 
        }
    }
    return -1; //直接返回查找失败
}
------------------------------------------------
case.2

int left_bound(int[] nums, int target) {
    
    
    int left = 0, right = nums.length - 1;
    while (left <= right) {
    
     // note1
        int mid = ((right - left) >> 1) + left;
        if (nums[mid] < target) {
    
    
            left = mid + 1;
        } else if (nums[mid] > target) {
    
    
            right = mid - 1; // note2
        } else if (nums[mid] == target) {
    
    
            right = mid - 1; // note3 不返回,锁定左侧边界
        }
    }
    // note4  最后检查 left 越界的情况
    if (left >= nums.length || nums[left] != target)
        return -1;
    return left;
}

------------------------------------------------------
case.3

int right_bound(int[] nums, int target) {
    
    
    int left = 0, right = nums.length - 1;
    while (left <= right) {
    
    
        int mid = ((right - left) >> 1) + left;
        if (nums[mid] < target) {
    
    
            left = mid + 1;
        } else if (nums[mid] > target) {
    
    
            right = mid - 1;
        } else if (nums[mid] == target) {
    
    
            left = mid + 1; // 不返回,锁定右侧边界
        }
    }
    // 最后要检查 right 越界的情况
    if (right < 0 || nums[right] != target)
        return -1;
    return right;
}

note1: 什么时候取 <= ?

大多数情况下,如果你的left和right初始值都是有效的索引,就用 <= 。
其实,简单来说就是 right 取 nums.length - 1, 这就可以 <=,
这时的while结束条件是 left = right + 1; 这个知识点很重要!!!

对于后面两个题目,在 nums[mid]==target时 start或者end的更新 跟这个点有很大关系!!

note2 & note 3: 更新start或者end的时候,带不带mid?

这里以第2题 寻找左边界举例子,第3题同理。
首先,note2这里 nums[mid] > target时,target一定不可能在[mid, right]这个闭区间出现了,所以 mid 自然取不到,right = mid - 1;

但是note3这里 为什么 nums[mid] == target 的时候 也不带mid了呢?

如果这里right = mid的话,对于 1 2 2 2 3 搜 1,会死循环(自己模拟一下)

你可能会担心 取right=mid - 1的话, mid左边如果不会再出现target了(此时的mid就是答案),就永远都回不到mid了,这不就找不到正确答案了吗?

实际上,不用担心,这就是为什么说上面 while结束条件是 left = right + 1;很重要!

我们注意到 while 的终止条件:left<=right 更清晰一点的是退出while的条件是 left=right+1

也就是说,如果此时这个mid左边没有target了,这个right在被赋值完mid-1以后,就不会再更新(往左前进了)因为此时[left, right]闭区间的所有数都小于target,更新的一直是left,right会一直停在mid-1的位置。

left一直往右前进更新,直到 left=right 时进入循环,发现nums[mid]还是<target, left继续前进,变成right + 1,退出循环!

退出循环的时候,left = right + 1, right+1不就是mid吗!!!!

note 4 返回啥 ?

    // note4  最后检查 left 越界的情况
    if (left >= nums.length || nums[left] != target)
        return -1;
    return left;

left>= nums.length 对应 找的target比nums所有数都大,left一直前进;
nums[left] != target 对应,找的target比nums所有数都小,此时left一直指向0,right一直往左走;

简单点,nums[mid]==target 的时候,您更新的那个指针,就返回哪个!


所以。其实后两个问题归根结底还是因为
nums[mid]==target的时候没有返回,而是继续搜索!!!

所以 此时的区间收缩不能按照上面nums[mid] > 或 < target的情况理论判断,要结合实际!

猜你喜欢

转载自blog.csdn.net/qq_43778308/article/details/108623206