关于二分查找的一些思考

278 阅读3分钟

最近在做二分查找的算法,发现二分查找并不是想象的那么简单,尤其是终止条件,已经mid的取值问题,总是让我晕头转向,下面以一个例子,说一下我对二分查找的理解归纳:

leetcode:剑指 Offer 53 - I. 在排序数组中查找数字 I

统计一个数字在排序数组中出现的次数。

示例 1:

输入: nums = [5,7,7,8,8,10], target = 8
输出: 2

示例 2:

输入: nums = [5,7,7,8,8,10], target = 6
输出: 0

限制:

0 <= 数组长度 <= 50000

来源:力扣(LeetCode) 链接:leetcode-cn.com/problems/za… 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

相信大家都已经理解了这个题目的意思:下面我说一下我当时做这个题目的思路

思路一

通过二分查找,找到target的索引index,然后从index左右扩展统计个数,代码如下:

public int search(int[] nums, int target) {
        int index = getIndex(nums,target);
        if(index < 0){
            return 0;
        }
        int count = 1;
        // 左侧可能还有
        for(int i = index-1 ; i >= 0; i--){
            if(nums[i] != target){
                break;
            }
            count++;
        }
        // 右侧可能也还有
        for(int i = index+1; i < nums.length; i++){
            if(nums[i] != target){
                break;
            }
            count++;
        }
        return count;
    }

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

执行结果:

执行结果:通过
执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
内存消耗:41.5 MB, 在所有 Java 提交中击败了27.56%的用户

当然这里我们不讨论执行效率,内存消耗情况,我们只是为了归纳出二分查找的一些常常让我们模糊的地方。这里我们主要讨论getIndex方法:这个方法可以归纳为一句话:从数组中找目标,找到返回index,找不到返回-1

数组寻值

问题(1):什么时候代码终止?

我们定义left=0,right=length-1,所以我们的查找范围是[left, right],闭区间,如果我们设置为 left < right 那么终止条件就是 left == right, 此时 [right, right], 恰好漏掉了一个 int[right]或者 int[left],如果我们设置为 left <= right, 那么终止条件就是 left == right+1,此时 [right+1, right] 范围为空

问题(2)int mid = (right - left) / 2 + left,为什么不直接 mid = (right+left)/2 ?

主要是考虑整型溢出的问题,这里我们不重点讨论(也没有讨论的必要,哈哈)。

问题(3)right为什么是mid - 1, left 为什么是 mid + 1 ?

主要是因为我们的范围是闭区间,而且我们已经知道了mid不符合条件;

思路二

找到左右index的位置,然后count = rightIndex - leftIndex - 1; 比如 ums = [5,7,7,8,8,10], target = 8, 4-3+1= 2

public int search(int[] nums, int target) {
        if(nums.length == 0){
            return 0;
        }
        int left = 0;
        int right = nums.length;
        // 寻找左侧边界
        while(left < right){
            int mid = left + (right-left)/2;
            if(nums[mid] == target){
                right = mid;
            }else if(nums[mid] < target){
                left = mid + 1;
            }else{
                right = mid;
            }
        }
        int leftindex = left;

        left = 0;
        right = nums.length;
        // 寻找右侧边界
        while(left < right){
            int mid = left + (right-left)/2;
            if(nums[mid] == target){
                left = mid + 1;
            }else if(nums[mid] < target){
                left = mid + 1;
            }else{
                right = mid;
            }
        }
        int rightindex = right - 1;

        int count = rightindex - leftindex + 1;
        return count;
        
    }

执行结果:

执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
内存消耗:41.1 MB, 在所有 Java 提交中击败了90.76%的用户

寻找左侧边界:

问题一:为什么这里的结束条件是 left < right

这里主要是因为 left=0, right=length, [left,right) 是一个左闭右开的区间,left == right是结束的条件,如果是 left <= right,那么结束的条件是 left == right+1, left可以取到right, right+1 造成数组下标越界, 这也是为什么right=mid 和 left=mid+1的原因

问题二:为什么最后左边界是left

因为最后退出是left==right,这是结束的条件,此时nums[mid]=target,所以最后返回left和right是一样的

寻找右侧边界

关于退出条件是同样的一个道理,最后为什么返回的是right-1,是因为最后退出的条件是left == right,而此时nums[mid] == target,而left == mid + 1,所以mid == left - 1也就是right -1;

总结

在写二分查找的时候,我们应该先理解区间的开闭,然后根据开闭确定退出条件,当然无论是左开右闭还是左闭右开,我们都可以替换为双闭区间,最后的返回数据要和mid的关系去对应起来,这样面对二分查找,我们再也不会为退出条件和返回参数而惆怅了。