二分查找实现细节

二分查找

第一部分

  • 原理
    与有序数组相关的问题,比如确定某个数是否在其中,采用二分查找可以达到log(n)的复杂度。

  • 原理虽然简单,但是要真正编码实现,而且无bug往往还不是那么容易。本人就实践过多次,每次写完后都要调试好几次才能去掉很多bug。但是在面试过程中,面试官看的就是你平时编码多不多,肯定不会让你有那么多机会调试,你最多可以心里调试一遍,这样往往就留下很多bug,一跑就可能出错。

  • 以下是我对bug出现最多的原因的总结
    (start、mid、end分别表示头、中点、尾端的游标或指针)


  1. 在while循环中出现死循环,原因是mid更新start,(start < end) true, 再次mid更新start,…,如此往复。
  2. 最终返回值游标采用mid,导致结果错误。

  • 原因和解决办法
    • 对1, 是因为最后出现了start = end - 1,即两者相邻的情形,此时mid = (start + end) / 2, 在int中,mid = start 之后,如果在if语句中出现 start = mid这样的更新游标语句,那么自然就会陷入死循环。因此我们要避免前面的游标直接由mid更新,即用start = mid + 1,此时将等于的情况挪给end更新来完成:end = mid, 因为这种情况绝不会导致死循环。
    • 对2, 是因为最后一次start = end后,将跳出while循环,而促使这个条件成立的,可能是由于start = mid + 1, 也就是说,最终值应该是更新后的 (start + end) / 2, 由于跳出了循环,将得不到更新后的值,而是前一步的mid值。所以最终的输出游标可以采用end,start应该也可以。
      e.g. 下面是在升序数组arr中,查找value是否在其中的一个例子,其中,start = mid + 1 使死循环不再出现。
    public boolean ascendingFind(int[] arr, int start, int end, int value){
        if (start > end) {
            return false;
         }
        int mid = 0;
        while(start < end){
        mid = (start + end) >> 1;
        if(arr[mid] < value){
            start = mid + 1;
        }else{
            end = mid;
          }
        }
        if (arr[end] == value) {
            return true;
        }
        return false;
    }

第二部分

  • 典型应用场景程序分析

经过上述总结后,本以为可以得心应手的,却发现在紧张状态下仍然对逻辑分析感觉很混乱,唯一有一点的好处是,我知道了很可能导致bug的点在start上面,还有对相邻的start和end进行分析。

  1. 给定数组a[](升序), 数m,求出m在a中最左边的位置/最右边的位置

最左边的位置,应该用a[mid] < m时,start = mid+1,为什么我们这么选而不用 a[mid] > m, end = mid - 1呢?因为根据第一部分的原理,我们为了避免最后start和mid交替赋值的死循环问题,我们想尽量在更新start时不要用start = mid。但有些情况下我们避免不了start = mid这样的情况,我们需要另外考虑。
这样我们就有:

    // 输出等于m的最左边的下标,如果m不在a中,则输出大于m的第一个下标
    public int leftMostEqual(int[] a, int m){
        int start = 0;
        int end = a.length - 1;
        int mid;
        while(start < end){
            mid = start + (end - start) / 2;
            if (a[mid] < m) {
                start = mid + 1;
            }else {
                end = mid;
            }
        }
        return end;

那最后的返回值取start、end还是mid呢?分析可以发现start = end,mid可能还没有更新,所以应该选择end。另外,当m不在a中时,上面的代码输出大于m的第一个下标。其实这里就对应了下面一个问题:

找出int[] a中大于等于m的最左边的下标

的解, 实现上完全一样,接口为:

public int leftMostGreatEqual(int[] a, int m)

接下来的问题为:找出m在a中的最右边的位置,
和上面问题的区别为,start = mid+1在这里用不了了,因为如果a[start] = m,我们不知道当前start是否是最后一个位置,还是第一个位置,所以不能往右移,只能用start = mid,这样为了避免死循环,我们需要改变while(start < end), 如下:

    // 输出等于m的最右边的下标,如果m不在a中,则输出小于m的最后一个下标
    public int rightMostEqual(int[] a, int m){
        int start = 0;
        int end = a.length - 1;
        int mid;
        while(start < end - 1){
            mid = start + (end - start) / 2;
            if (a[mid] > m) {
                end = mid - 1;
            }else {
                start = mid;
            }
        }
        if(a[end] == m)
            return end;
        else {
            return start;
        }
    }

我们将终止条件变成了start < end - 1防止start和end发生相邻,当start和end相邻时,循环结束。
那么我们应该取end还是start呢?通过举例可以发现,a[end]肯定不会小于m,所以我们可以从end开始尝试和目标值m比较,再返回正确的值。

另外,返回小于等于某给定数的最右边的下标 的问题,也和这个问题解法完全一样,他们的本质是一样的。

就是这样,欢迎各位提出问题和意见。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值