二分查找 总结(二)

前篇链接
在这里插入图片描述

二分查找 思路二

二分查找的第二种思路
在循环体内排除一定不存在的目标元素的区间
在这里插入图片描述
此题的另外两种写法:

public class Solution {
  
    // 「力扣」第 704 题:二分查找

    public int search(int[] nums, int target) {
        int len = nums.length;

        int left = 0;
        int right = len - 1;
        // 目标元素可能存在在区间 [left, right]
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] < target) {
                // 下一轮搜索区间是 [mid + 1, right]
                left = mid + 1;
            } else {
                // 下一轮搜索区间是 [left, mid]
                right = mid;
            }
        }

        if (nums[left] == target) {
            return left;
        }
        return -1;
    }
}
public class Solution {
  
    // 「力扣」第 704 题:二分查找

    public int search(int[] nums, int target) {
        int len = nums.length;

        int left = 0;
        int right = len - 1;
        while (left < right) {
            int mid = left + (right - left + 1) / 2;
            if (nums[mid] > target) {
                // 下一轮搜索区间是 [left, mid - 1]
                right = mid - 1;
            } else {
                // 下一轮搜索区间是 [mid, right]
                left = mid;
            }
        }

        if (nums[left] == target) {
            return left;
        }
        return -1;
    }
}

分析这两种写法和思路 1 的写法有何不同:
循环可以继续的条件是 while (left < right) ,这是一个很重要的标志。为什么是严格小于呢?我们上一节说过,当 left == right ,左边界和右边界重合的时候,区间里只有 1 个元素时候,二分查找的逻辑还需要继续下去;而现在大家看到的这个解法在 left == right 重合的时候就退出循环,这一点表示区间里只剩下一个元素的时候,有可能这个元素就是我们要找的那个元素。这一点与二分查找算法的思路 2(在循环体中排除元素)是一致的:排除了所有错误的答案,如果题目告诉我们只有 1 个目标元素,那么剩下的这个元素就一定是目标元素。

在退出循环以后,还需要单独做一次判断;那么这样的写法是不是更麻烦了呢?其实不是的:

  • 首先,有些算法问题根据题目的意思,要找的目标元素一定落在题目给的区间里,那么最后的这一步判断可以省略;
  • 并且我们看到这个写法只把区间分成了两个部分,其实在我们编写代码的时候要考虑的因素会更少。这两个区间没有交集,并且它们合起来组成了整个当前待搜索的区间。因此,在思考缩小待搜索区间的逻辑的时候,只需要考虑其中一种情况,另一种情况得到的区间就正好是上一个区间的反面区间;
  • 那么如何考虑缩小问题的区间呢?通常的思路是:先思考要找的数的性质,然后对这个性质取反,也就是:先讨论看到的中间位置的元素在什么情况下不是目标元素,采用这样的思路解决问题会容易一些;

友情提示:生活中的一些事情我们往往很清楚自己不需要什么,但是说不清楚自己真正需要什么。从中间位置的元素在什么情况下不是目标元素考虑,使得问题变得简单也是类似的事实。

  • 例如上题,我们就是要找 == target 的元素。对这个性质取反,就是 != target,也就是 < target 或者 > target 的时候,这两个条件选择其中一个,都可以缩小问题的区间,于是就有了上面两版代码的写法;
  • 这里细心的朋友可能发现了:int mid = left + (right - left + 1) / 2; 有个上取整,这是必须的吗?答案是必须的,如果不加的话,这一版代码会出现死循环。这个注意事项,不用刻意去记。我们在写完一个算法的时候,通常来说都会拿示例中的测试用例去执行一下我们所写的代码,在测试的时候,就能够意识到要调整下取整成为上取整。

向上取整的原因

取中间数可能需要上取整的原因
我们解释为什么这里需要上取整。这个二分法的思路根据中间数的值把区间分为两个部分:

  • 一定不存在目标元素的部分;
  • 可能存在目标元素的部分。
    在这里插入图片描述
    mid 被分到左边区间
    这个时候区分被分为两部分:[left, mid][mid + 1, right],对应设置边界的代码为 right = midleft = mid + 1;

mid 被分到右边区间
这个时候区分被分为两部分: [left, mid - 1][mid, right],对应设置边界的代码为 right = mid - 1left = mid

注意:这种情况下,当搜索区间里只剩下两个元素的时候,一定要将取中间数的行为改成上取整,也就是在括号里加 1。

这是因为 [left, right] 区间里只剩下两个元素的时候,如果是取中间数 mid 是下取整,一旦进入 left = mid 这个分支,区间就不会再缩小,下一轮搜索的区间还是 [left, right] ,下一次循环取 mid 还是看到了 left ,由于逻辑和上一轮循环一样,因此搜索区间不会缩小,就这样一直下去,这是一个死循环。

解决方案也很简单,在最后一次循环的时候,把取中间数的时候修改为上取整。那么是不是要做一次判断,什么时候到了最后一轮循环呢?没有必要,整个循环体内,上取整就可以了。这个结论很重要,希望大家能够理解这里上取整的原因。根据循环体里,中位数被分到哪个区间,来决定取中间数的时候是否上取整

友情提示:这一点是需要调试深刻理解的,大家在遇到死循环的时候,把变量 left 和 right 和相关变量的值做一个打印输出,就会看得非常清楚

编码要点


编码要点

  • 循环终止条件写成:while (left < right) ,表示退出循环的时候只剩下一个元素;

  • 在循环体内考虑如何缩减待搜索区间,也可以认为是在待搜索区间里排除一定不存在目标元素的区间;

  • 根据中间数被分到左边和右边区间,来调整取中间数的行为;

  • 如何缩小待搜索区间,一个有效的办法是:从 nums[mid] 满足什么条件的时候一定不是目标元素去考虑,进而考虑 mid 的左边元素和右边元素哪一边可能存在目标元素。一个结论是:当看到 left = mid 的时候,取中间数需要上取整,这一点是为了避免死循环;

  • 退出循环的时候,根据题意看是否需要单独判断最后剩下的那个数是不是目标元素。(有些题可能待查元素一定存在,所以就不需要判断)
    边界设置的两种写法:

  • right = midleft = mid + 1int mid = left + (right - left) / 2; 一定是配对出现的;

  • right = mid - 1left = midint mid = left + (right - left + 1) / 2; 一定是配对出现的。

这一点不需要记忆,大家只要一直思考目标元素可能存在的区间就可以了。就像我们在代码注释里展示给大家的一样。因为下一轮搜索的区间是 [left, mid] 所以这个时候设置 right = mid 。当前这条性质不满足的时候,既然整个区间是 [left, right] 区间里,第一种情况所在区间是 [left, mid] ,那么另外一种情况对应的区间是 [mid + 1, right] ,两个区间合起来就是整个区间 [left, right] 。同理,去理解 right = mid - 1left = mid 这两个边界设置。

适用范围


适用范围
这种二分查找的思路,对于查找边界问题,会使得思考的过程变得简单。

二分查找的思路 1,有的时候要考虑返回是 left 还是 right,在循环体内有 3 个分支,如何分类讨论,有些时候并不那么容易。

思路 2 的分支只有 2 个,其中一个思考对了,另外一个就可以直接得到。其实思路 2 更符合二分这个语义,我们就是将区间一分为二地去看待,一部分一定不存在目标元素,我们把它排除掉了,我们在另外一个可能存在目标元素的区间里继续查找

希望大家能够通过练习体会这种思路写二分查找的细节,使用它写对所有的二分查找的问题。这个思路在维基百科上有一个非常简短的描述,那就是「将判断相等的步骤放到算法末尾,虽然将平均迭代次数增加 1 次,但是每次迭代中的比较次数减少了1 次」。我们来理解一下这个说法:

  • 如果数组里一定存在目标元素,那么第一种思路运气好的话,可能很快就可以返回;但是在数组里一定找不到目标元素的时候,两种思路下的循环体执行的次数是一样的,这就是思路 2 比思路 1 平均迭代次数增加 1 次的意思;
  • 如果这个二分查找的问题比较复杂,要你找一个可能在数组里不存在,或者是找边界这样的问题,使用思路 2 ,在循环体内排除一定不存在目标元素的区间会更简单一些。

接下来我们再介绍一种二分的写法,相信只要是理解了前两种思路,这种写法就不难理解了。

public class Solution {
    
    // 「力扣」第 704 题:二分查找

    public int search(int[] nums, int target) {
        int len = nums.length;

        int left = 0;
        int right = len - 1;
        // 目标元素可能存在在区间 [left, right]
        while (left + 1 < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] < target) {
                left = mid;
            } else {
                right = mid;
            }
        }

        if (nums[left] == target) {
            return left;
        }

        if (nums[right] == target) {
            return right;
        }
        return -1;
    }
}

代码说明:

  • 这种思路循环可以继续的条件是 while (left + 1 < right) ,在待搜索区间剩下两个元素的时候,退出循环,所以它不需要考虑死循环的问题;但是正是由于退出循环的时候区间里有 22 个元素,所以在退出循环的时候一定得做判断;
  • 在循环体内分为 3 个分支,这一点和思路 1 是一样的;在循环体内,就没有 mid + 1 和 mid - 1 这样的表达式了。把中间数 mid 全部纳入下一轮要考虑的范围里。

这种写法不建议,理由如下:

  • left + 1 < right 这种写法不是很自然;
  • 退出循环的时候,一定要处理两个元素区间是两个元素的逻辑,这一步是附加的逻辑,是有可能出错的,相对于思路 2 是不太好的做法;
  • 对于 mid 是不是在下一轮要考虑的区间里,这件事情只要思路清晰,是可以准确得出结论的,而这种写法恰好屏蔽了这些细节。

当然,对于做题来讲,我们介绍的 3 种写法思路上是一致的,大家需要通过练习,来掌握这些写法对于细节的处理。

while循环中,先写 if (nums[mid] < target)说明利用的是划分区间为 [left,mid] ;[mid+1,right]; mid不需要上取整;
if (nums[mid] > target) 则划分区间为[left, mid - 1];[mid, right]需要上取整,否则会出现死循环,即出现left=mid时,考虑使用上取整

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值