二分查找——从入门到精通

最近开始用java刷算法,很多地方不习惯,包括之前很熟悉的二分查找,都会出现各种问题,因为java很多方法都封装了,你得重新了解它是怎么实现的,看工具开发者编写的源码真的能更好地学习和理解计算机,感觉自己之前的思路都弱爆了,拿二分法来举例:

入门版:

传统的二分法代码,包括很多教科书上都是这么写的:

public class Solution {

    public int searchInsert(int[] nums, int target) {//Integer
        int len = nums.length;
        if (nums[len - 1] < target || nums[0] > target) {
            return len;
        }

        int l = 0;
        int r = len - 1;

        while (l <= r) {
            int mid = (l + r) / 2;
            // 等于的情况最简单,我们放在第 1 个分支进行判断
            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] < target) {
                // 题目要我们返回大于或者等于目标值的第 1 个数的索引
                // 此时 mid 一定不是所求的左边界,
                // 此时左边界更新为 mid + 1
                l = mid + 1;
            } else {
                // 既然不会等于,此时 nums[mid] > target
                // mid 也一定不是所求的右边界
                // 此时右边界更新为 mid - 1
                r = mid - 1;
            }
        }
        // 注意:一定得返回左边界 l,
        // 如果返回右边界 r 提交代码不会通过
        // 理由是对于 [1,3,5,6],target = 2,返回大于等于 target 的第 1 个数的索引,此时应该返回下标1
        // 在上面的 while (l <= r) 退出循环以后,r < l,r = 0 ,l = 1
        // 根据题意应该返回 l,
        // 如果题目要求你返回小于等于 target 的所有数里最大的那个索引值,应该返回 r

        return l;
    }
}

进阶版:

最近在力扣上刷题看到了一些前辈总结的二分查找模板,感觉很有意思:

把循环可以进行的条件写成 while(l<r),这样的话当退出循环时,不用纠结返回 l 还是r,如果你不确定你要找的数一定在左边界和右边界所表示的区间里出现,那么也没有关系,只要在退出循环以后,再针对 nums[l] 或者 nums[r] 单独作一次判断,看它是不是你要找的数即可。

public class Solution {

    public int searchInsert(int[] nums, int target) {
        int len = nums.length;
        if (len == 0) {
            return 0;
        }
        if (target > nums[len - 1]) {
            return len;
        }
        int left = 0;
        int right = len - 1;
        while (left < right) {
            int mid = left + (right - left) / 2;//左中位数
            if (nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        return right;
    }
}

注意点:

1、当 l 和 r 是很大的整数的时候,你写 int mid = (l + r) / 2; 这里 l + r 就有可能超过 int 类型能表示的最大值,因此使用 mid = l + (r - l) / 2 可以避免这种情况。事实上 mid = l + (r - l) / 2 在 r 很大,并 l 是负数且很小的时候, r - l 也有可能超过 int 类型能表示的最大值,只不过一般情况下 l 和 r 表示的是数组索引值,l 是非负数,因此 r - l 溢出的可能性很小。

 2、先考虑能把“中位数”排除在外的逻辑,而不能排除“中位数”的逻辑放在 else 分支里(用来缩小循环区间,避免进入死循环

分支是左区间不收缩的时候,选中位数选右中位数,因为如果你选左中位数,会出现死循环。mid = l + (r - l+1) / 2;

分支是右区间不收缩的时候,选中位数选左中位数,因为如果你选右中位数,会出现死循环。mid = l + (r - l) / 2;

终极进阶版:

再来看看JDK8中计算mid方法是怎么写的吧:

private static <T>
    int indexedBinarySearch(List<? extends Comparable<? super T>> list, T key) {
        int low = 0;
        int high = list.size()-1;

        while (low <= high) {
            int mid = (low + high) >>> 1;
            Comparable<? super T> midVal = list.get(mid);
            int cmp = midVal.compareTo(key);

            if (cmp < 0)
                low = mid + 1;
            else if (cmp > 0)
                high = mid - 1;
            else
                return mid; // key found
        }
        return -(low + 1);  // key not found
    }

JDK8 中采用 int mid = (low + high) >>> 1 这种写法,重点在 ">>>" 。顺便来复习一下无符号右移>>>(高位补0)和有符号右移>>(高位补原符号位),当32位整形值在溢出后,变为负数,符号位变为1,但是当经过无符号右移一位后,高位补0,结果仍是正确的,你就说牛不牛逼,巧不巧妙,当然还有一点:位运算又比用除法快,是不是一举多得?

参考:https://www.liwei.party/2019/06/17/leetcode-solution-new/search-insert-position/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值