二分查找算法总结

解题思路

核心要素

  • 注意区间开闭,三种都可以

  • 循环结束条件:当前区间内没有元素。

  • 下一次二分查找区间:不能再查找(区间不包含)mid,防止死循环

  • 返回值:大于等于target的第一个下标(注意循环不变量)

  • 循环结束条件取决于初始化条件,下次赋值条件也取决于初始化条件。判断大小取决于题目条件。

  • 有序数组中二分查找的四种类型(下面的转换仅适用于数组中都是整数)

  1. 第一个大于等于x的下标: low_bound(x)
  2. 第一个大于x的下标:可以转换为第一个大于等于 x+1 的下标 ,low_bound(x+1)
  3. 最后一个一个小于x的下标:可以转换为第一个大于等于 x 的下标左边位置, low_bound(x) - 1;
  4. 最后一个小于等于x的下标:可以转换为第一个大于等于 x+1 的下标左边位置, low_bound(x+1) - 1;
  • 问题:返回有序数组中第一个≥8的数的位置。如果所有数小于8,返回数组长度
    • 暴力做法:遍历每一个数,询问它是否≥8?
      在这里插入图片描述
  • 二分法:L和R分别指向询问的左右边界,即闭区间[L,R]
    • M指向当前询问的数
    • 红色背景表示false,即<8
    • 蓝色背景表示false,即≥8
    • 无色表示不确定

在这里插入图片描述

  • 情况1:Mid < target
    • 那么左侧全部是小于target: [L,M] 都是红色
    • 剩余不确定的区间为:[M + 1, R]
    • 下一步:L 赋值为:M + 1
    • 同时也说明了L -1 指向的也一定是红色

在这里插入图片描述

在这里插入图片描述

  • 情况二:Mid 大于等于target,
    • M是蓝色,[M,R] 都是蓝色
    • 剩余不确定区间为[L,M -1]
    • 因此下一步:R 赋值为M - 1
    • 同时说明R + 1指向的一定是蓝色
      在这里插入图片描述
  • 此时闭区间为[L,R] 中还有一个元素还是需要继续判断

在这里插入图片描述

  • 循环的终止条件为:L < R, 等于相当于还有一个条件没有判断,例如上面
  • 关键是:循环不变量
    • L - 1始终是红色
    • R + 1 始终是蓝色
  • 根据循环不变量,R + 1是我们要找的答案
  • 由于循环结束后R + 1 = L
  • 所以答案也可以用L表示。

闭区间写法

    /**
     * 查找第一个大于等于target的元素
     * 要求: nums是非递减的,即nums[i] <= nums[i+1]
     * 返回最小的满足nums[i] >= target的下标i
     * 如果不存在,返回nums.length
     * 
     * @param nums
     * @param target
     * @return
     */

    private int lowerBound(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 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 - 1]
                right = mid - 1;
            }
        }
        // 思考一下为什么不去right呢,终止条件是left > right。  提示:假设left == right时,在循环一次。就知道结果了
        return left;
    }

左闭右开

 private int lowerBound2(int[] nums, int target) {
        int left = 0;
        int right = nums.length;
        // 左闭右开区间 [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, 也是将mid排除在外了。
                // left是闭区间,还未判断出来,但是right是开区间
                right = mid;
            }
        }
        //  其实返回left或者right都可以
        return left;
    }

开区间

    private int lowerBound3(int[] nums, int target) {
        int left = -1;
        int right = nums.length;
        // 左闭右开区间 (left, right)
        while (left + 1 < right) {  // 注意: 区间不为空
            // 防止溢出
            int mid = left + (right - left) / 2;
            if (nums[mid] < target) {
                // (mid , right)
                left = mid;
            } else {
                // (left, mid)   # 此处注意,其实right = mid, 也是将mid排除在外了。
                // left是闭区间,还未判断出来,但是right是开区间
                right = mid;
            }
        }
        //  其实返回left + 1或者right都可以
        return right;
    }
  • 要素:闭区间还是开区间是根据你的初始化条件决定的。至于下次left和right的变量是 +1还是-1还是赋值成mid,其实是看你的区间,如果是开区间(left, right)那就是没有判断到,因此赋值成mid。若是闭区间则是取到了,因此是 mid+1获取mid -1,但是闭区间需要多判断一次。
    private int lowerBound4(int[] nums, int target) {
        // 初始化条件:[left, right]
        int left = 0;
        int right = nums.length - 1;

        while (left <= right) {  // 区间不为空,此刻left == right,其实还是有一个元素的
            // 防止溢出
            int mid = left + (right - left) / 2;
            // 表示:right到mid,包含mid大于等于target, 其实是满足条件的,因此目标位置肯定在mid已经mid的左边,因此right向左移动
            if (nums[mid] > target || nums[mid] == target) {
                // [left, mid - 1]
                right = mid - 1;
            }
            // 表示:左侧包含mid全部小于target,此刻不满足条件,left继续右移动。
            else {
                // [mid + 1, right]
                left = mid + 1;
            }
        }
        // 终止条件为:left > right。
        return left;
    }

扩展

一共有四种情况

第一个大于等于 target,如上所示,结果为i

在这里插入图片描述

第一个大于target,对于整数可以转换成大于等于 target + 1, 可将新的target = target + 1

在这里插入图片描述

  • 这是第一种解法:大于8,改成大于等于9,在用上面的方法lowerBound(nums, target + 1)
  • 第二种解法:根据条件改编判断条件
    private int lowerBound4(int[] nums, int target) {
        // 初始化条件:[left, right]
        int left = 0;
        int right = nums.length - 1;

        while (left <= right) {  // 区间不为空,此刻left == right,其实还是有一个元素的
            // 防止溢出
            int mid = left + (right - left) / 2;
            // 标识 满足条件当前值大于target
            if (nums[mid] > target) {
                // [left, mid - 1]
                right = mid - 1;
            }
            // 表示:左侧包含mid全部小于target,此刻不满足条件,left继续右移动。
            else {
                // [mid + 1, right]
                left = mid + 1;
            }
        }
        // 终止条件为:left > right。
        return left;
    }

最后一个小于target, 可以转换成大于等于target 左边那个数i - 1 。若是第一个小于target,,只能是降序
最后一个小于等于target, 可以转换成大于target左边那个数。可将target = target + 1, 取 i - 1

34.在排序数组中查找元素的第一个和最后一个位置

  • 【大于等于target的位置,大于等于target + 1的位置 - 1】
    public int[] searchRange(int[] nums, int target) {
        int start = lowerBound(nums, target);
        if (start == nums.length || nums[start] != target) {
            return new int[]{-1,-1};
        }
        int end = lowerBound(nums, target + 1) - 1;
        return new int[]{start, end};
    }

    private int lowerBound(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;
        while (left <= right) {
            int mid = left + (right - left) /2;
            if (nums[mid] > target || nums[mid] == target) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        return left;
    }
  • 【大于等于target的位置,大于target的位置 - 1】
    public int[] searchRange(int[] nums, int target) {
        int start = lowerBound1(nums, target);
        if (start == nums.length || nums[start] != target) {
            return new int[]{-1,-1};
        }
        int end = lowerBound2(nums, target) - 1;
        return new int[]{start, end};
    }

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


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

35. 搜索插入位置

public int searchInsert(int[] nums, int target) {
        // 查找第一个大于等于 target的值
        int left = 0;
        int right = nums.length -1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] > target || nums[mid] == target) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        return left;
    }

704. 二分查找

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

744. 寻找比目标字母大的最小字母

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

2529. 正整数和负整数的最大计数

	public int maximumCount(int[] nums) {
        //找到第一个大于等于0的位置,找到第一个大于等于1的位置
        int pos1 = search(nums, 0);
        int pos2 = search(nums, 1);
        return Math.max(pos1, nums.length - pos2);
    }

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

1835. 两个数组间的距离值

    public int findTheDistanceValue(int[] arr1, int[] arr2, int d) {
        Arrays.sort(arr2);
        int cnt = 0;
        for (int x : arr1) {
            int p = binarySearch(arr2, x);
            boolean ok = true;
            if (p < arr2.length) {
                ok &= arr2[p] - x > d;
            }
            if (p - 1 >= 0 && p - 1 <= arr2.length) {
                ok &= x - arr2[p - 1] > d;
            }
            cnt += ok ? 1 : 0;
        }
        return cnt;
    }

    public int binarySearch(int[] arr, int target) {
        int low = 0, high = arr.length - 1;
        if (arr[high] < target) {
            return high + 1;
        }
        while (low <= high) {
            int mid = (high - low) / 2 + low;
            if (arr[mid] >= target) {
                high = mid - 1;
            } else {
                low = mid + 1;
            }
        }
        return low;
    }

2300. 咒语和药水的成功对数

	public int[] successfulPairs(int[] spells, int[] potions, long success) {
        Arrays.sort(potions);
        int[] result = new int[spells.length];
        for (int i = 0; i < spells.length; i++) {
            int pos = search(potions, spells[i], success);
            result[i] = potions.length - pos;
        }
        return result;
    }

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

2389. 和有限的最长子序列

	public int[] answerQueries(int[] nums, int[] queries) {
        Arrays.sort(nums);
        int[] f = new int[nums.length];
        f[0] = nums[0];
        for (int i = 1; i < nums.length; i++) {
            f[i] = f[i - 1] + nums[i];
        }
        int[] res = new int[queries.length];
        for (int i = 0; i < queries.length; i++) {
            res[i] = search(f, queries[i] + 1);
        }
        return res;
    }

    private int search(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] >= target) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        return left;
    }
  • 11
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值