二分搜索

1. 二分搜索的概念

  • 二分查找是一种在有序数组中查找某一特定元素的搜索算法。

  • 二分查找的原理是,将目标元素和查找范围的中间值做比较,如果目标元素等于中间值,则查找结束;如果目标元素大于或小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。这样每一次比较都使搜索范围缩小一半。

2. 在有序数组中查找 num 是否存在

  • 需求

    给定一个有序数组 arr , 给定一个目标值 target , 查找 targetarr 中是否存在.

  • 思路

    对于有序数组中的数值查找, 首先想到使用二分法实现.

  • 关键代码

    /**
     * 查找target在arr中是否存在
     *
     * @param arr    有序数组
     * @param target 目标值
     * @return true:表示target在arr中存在, false表示不存在
     */
    public static boolean exist(int[] arr, int target) {
        if (arr == null || arr.length == 0) { // 当数组为空时,无需查找,返回false.
            return false;
        }
    
        int L = 0; // 查找范围区间左侧的元素的下标,从0开始
        int R = arr.length - 1; // 查找范围区间右侧元素的下标,从arr的末尾开始
        int M = 0; // 中间元素的位置,默认是0
    
        // <= 表示这是一个 [左闭,右闭] 的区间范围.
        // 不使用 < 是考虑到在查找的过程中会动态调整L和R的值,有可能出现 L和R 重合的情况,即区间范围只有一个值的情况.
        while (L <= R) {
            M = (L + R) / 2; // 获取查询区间的中点
            // 下面这种方式计算结果与上面相同,但是可以避免 L + R 溢出的问题
            // M = L + ((R-L)>>1);
    
            if (arr[M] == target) { // 中点的值正好等于target,找找到了该元素,返回true.
                return true;
            } else if (arr[M] < target) { // 目标值大于中点的值,则从中点+1的位置往右查找
                L = M + 1;
            } else { // 目标值小于中点的值,则从中点-1的位置往左查找
                R = M - 1;
            }
        }
    
        // 如果while循环没有找到target,则返回false;
        return false;
    }
  • 完整测试代码

    https://github.com/GeorgeChan95/algorithm/blob/master/src/main/java/com/george/binarySearch/FindNumber.java

3. 在有序数组中查找 >=num 的最左位置

  • 需求

    给定一个从小到大的有序数组 arr, 给定一个目标值 target, 查找在这个数组中 >=target的 最左侧的元素的下标值.

  • 实现思路

    • 有序数组中的操作,首先考虑二分查找.找到数组中间位置的数据,比较中间值是否大于等于目标值, 如果是则表明目标值一定在中间元素的左边.此时就需要向左查找.
    • 如果中间值小于目标值,则表明目标值在中间值的右边,此时就需要往右查找.
  • 关键代码

    /**
     * 使用二分查找法,获取 >=target 的最左边的数的下标
     *
     * @param arr    目标数组, 例如: [1,2,3,4,5,6,7,8,9]
     * @param target 目标值, 例如: 2
     * @return
     */
    public static int findLeft(int[] arr, int target) {
        int L = 0; // 查找范围区间左侧的元素的下标,从0开始
        int R = arr.length - 1; // 查找范围区间右侧元素的下标,从arr的末尾开始
        int M = 0; // 中间元素的位置,默认是0
        int ans = -1; // 查找到的目标值的下标,没有找到则返回-1
        if (arr == null || arr.length == 0) { // 当数组为空时,无需查找,返回 -1.
            return ans;
        }
        while (L <= R) { // 确保查找范围区间元素数量 >=1 个
            M = (L + R) / 2; // 获取查询区间的中点
            // 下面这种方式计算结果与上面相同,但是可以避免 L + R 溢出的问题
            // M = L + ((R-L)>>1);
    
            if (arr[M] >= target) { // 如果中点元素值 >=target 说明中点往左可能还有别的数也 >=target,此时记录下标值,继续向中点左侧找,直到找到最左边 >=target 的值.
                R = M - 1; // 区间往左搜小
                ans = M; // 记录当前下标值
            } else { // 如果中点元素值 <target, 说明目标元素在中点往右的位置,则需要向右查找
                L = M + 1;
            }
        }
        // >=target最左边的数的下标
        return ans;
    }
  • 完整测试代码

    https://github.com/GeorgeChan95/algorithm/blob/master/src/main/java/com/george/binarySearch/FindLeft.java

4. 在有序数组中查找 <=num 的最右位置

  • 需求

    给定一个从小到大的有序数组 arr, 给定一个目标值 target, 查找在arr数组中 <=target 的最右侧(最大)的元素的下标值.

  • 思路

    • 有序数组中的操作,首先考虑二分查找.找到数组中间位置的数据,比较中间值是否小于等于目标值, 如果是则表明目标值一定在中间元素的右边.此时就需要向右查找.
    • 如果中间值大于目标值,则表明目标值在中间值的左边,此时就需要往左查找.
  • 关键代码

    /**
      * 使用二分查找,数组arr中 <=num 最右边的数的下标值
      *
      * @param arr    从小到大的有序数组
      * @param target 要查询的目标值
      * @return
      */
    public static int findRight(int[] arr, int target) {
        int L = 0; // 数组中查找的左侧区间范围,从0开始
        int R = arr.length - 1; // 数组中查找的右侧区间范围,从数组的末尾开始
        int M = 0; // 初始中间位置
        int ans = -1; // 响应结果,-1表示没有找到该元素
    
        if (arr == null || arr.length == 0) { // 数组为空,不再继续查找
            return ans;
        }
    
        while (L <= R) { // 确保查找范围区间元素数量 >=1 个
            M = L + (R - L) / 2; // 获取中间位置
            if (arr[M] <= target) { // 中间值与目标值进行比较,如果<=target,则标记下当前位置,继续向右查找,如果 >target则向左继续查找
                ans = M; // 标记当前下标
                L = M + 1; // 左侧查询区间移动到 M+1 位置,继续往右查找
            } else {
                R = M - 1; // 右侧查询区间移动到 M-1 位置,继续往左查找
            }
        }
        // 返回查询结果
        return ans;
    }
  • 完整测试代码

    https://github.com/GeorgeChan95/algorithm/blob/master/src/main/java/com/george/binarySearch/FindRight.java

5. 无序数组使用二分法寻找峰值

  • 题目描述

    • 峰值元素是指其值严格大于左右相邻值的元素。

    • 给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。

    • 你可以假设 nums[-1] = nums[n] = -∞

    • 你必须实现时间复杂度为 O(log n) 的算法来解决此问题。

  • 示例 1:

    • 输入:nums = [1,2,3,1]
    • 输出:2
    • 解释:3 是峰值元素,你的函数应该返回其索引 2。
  • 示例 2:

    • 输入:nums = [1,2,1,3,5,6,4]
    • 输出:1 或 5
    • 解释:你的函数可以返回索引 1,其峰值元素为 2;
      或者返回索引 5, 其峰值元素为 6。
  • 提示:

    • 1 <= nums.length <= 1000 (数组长度在 1~1000范围内)
    • -231 <= nums[i] <= 231-1 (数组元素值不超过int最大值)
    • 对于所有有效的 i 都有 nums[i] != nums[i + 1] (数组中任意相邻的两个数不相等)
  • 思路

    • 虽然寻找峰值这样的题目,不是在有序数组中,但具体题目的要求,依然可以使用二分查找解决这个问题.

    • 如果一个数组中存在峰值,那么从这个数组的第0个位置到最后一个位置所经过的数值,连成线一定是时高时低(想象着把数值连城折线图).

    • 可以先从中间位置开始找峰值, 如果中间位置的数比左右两侧的数都要大,则找到峰值.

    • 如果中间位置比左侧的数据小,则表示在左边一定至少存在一个峰值. 因为哪怕是最极端的情况, 从0位置到中间位置数据是逐个减小的,那么0位置就是峰值. 所以至少会有一个峰值. 按照这个思路继续使用二分法向左边查找峰值,直到找到一个峰值.

    • 如果中间位置比右侧的数据小,则表示在右边至少存在一个峰值,理由和上一条相同.

  • 关键代码

    /**
      * 使用二分查找,找到数组中的峰值
      * @param arr 数组
      * @return
      */
    public static int findPeak(int[] arr) {
        int n = arr.length; // 数组的长度
        if (n == 1) { // 数组长度为1,那么这个元素就是峰值,因为: arr[-1] = arr[n] = 无穷小
            return 0;
        }
    
        if (arr[0] > arr[1]) { // 满足条件: arr[-1] = 无穷小
            return 0;
        }
    
        if (arr[n - 1] > arr[n - 2]) { // 满足条件: arr[n] = 无穷小
            return n - 1;
        }
    
        int L = 1; // 二分查找的左侧区间范围, 从 1 开始
        int R = n - 2; // 二分查找的右侧区间范围, 从 n-2 开始
        int M = 0; // 中间索引,默认:0
        int ans = -1; // 峰值所在位置索引
    
        while (L <= R) {
            M = L + ((R - L) / 2); // 计算中间值索引
            if (arr[M + 1] > arr[M]) { // M到n-1区域一定存在峰值,向右查找
                L = M + 1;
            } else if (arr[M - 1] > arr[M]) { // 0到M区域一定存在峰值,向左查找
                R = M - 1;
            } else {
                ans = M; // 找到峰值, 跳出循环
                break;
            }
        }
        return ans;
    }
  • 测试地址

    https://leetcode.cn/problems/find-peak-element/

6. 技巧

  • 二分查找需要根据左右两个查询区间找到中间位置的下标, 但是当查询范围特别大时,比如数组长度: 231 , 左右区间相加就会溢出,导致计算出错. 为了避免数值溢出, 不能使用 (L+R)/2 的方式计算中间值, 需要用 L+(R-L)/2 的方式.
  • L + ((R-L) >> 1) 也等同于 L+(R-L)/2 , 样做是利用了位运算 >> 的特点, 右移1位, 就等于除以2

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 george_95@126.com