二分查找+二分答案(Java)


二分查找

二分查找也叫折半查找,在一个有序(递增或者递减)的数列里寻找一个满足要求的数字

做法

如果我们要在一个有单调性的数组里查找某个数target,可以通过当前区间的最左边位置 left 和最右边位置right计算出中间位置 mid,再拿mid位置的元素和要找的元素 target 元素比较大小,来判断元素在在 mid 的左边还是右边,一次砍一半,重复此操作,找到可能的元素或者没有找到

下标问题

我们最直观的计算mid下标就是 通过 (left+right)/2,那么是否要考虑奇数和偶数的问题呢?

因为计算机在计算整数除法的时候是向下取整的 (1+3)/2 == (1+4)/2 的,无论是

边界问题

因为数组下标是从0开始的,为了方便计算mid下标,我们一般采取左闭右开区间,[left,right]

在这里插入图片描述

left 一般从0开始,不要管,

关键就是 right 的取值就关系到边界问题,也就是二分的结束条件

如果 right一般取的是数组最后一个下标,也就是数组长度 length-1

那么 二分的结束条件就是 left<=right,如果不取等于就会出现结果被忽略的情况

在这里插入图片描述

图解

代码实现

注意:((right-left)>>1)+left 等价于 (left+right)/2

递归

public static boolean dichotomy(int[] arr,int left,int right,int target) {
    
    
        // 边界判断
        if (left > right) {
    
    
            return false;
        }
        int mid = ((right-left)>>1)+left;
        if (arr[mid] > target) {
    
    
            // 如果中间位置的数比要找的数还要大,说明要找的数组在左半部分
            return dichotomy(arr,left,mid-1,target);
        } else if (arr[mid] < target) {
    
    
            // 如果中间位置的数比要找的数字还要小,说明要找的数在数组的右半部分
            return dichotomy(arr, mid+1, right, target);
        } else {
    
    
            return true;
        }
    }

迭代

public static boolean dichotomy(int[] arr,int target) {
    
    
        int left = 0;
        int right = arr.length-1;
        int mid = 0;

        while (left <= right) {
    
    
            // 计算中间下标
            mid = ((right-left)>>1) + left;

            if (arr[mid] > target) {
    
    
                // 如果中间位置的数比要找的数还要大,说明要找的数组在左半部分
                right = mid-1;
            } else if (arr[mid] < target) {
    
    
                // 如果中间位置的数比要找的数字还要小,说明要找的数在数组的右半部分
                left =  mid+1;
            } else {
    
    
                return true;
            }
        }
        // 如果没有找到
        return false;
    }

复杂度分析

因为二分查找都是每次将区间长度砍半,就是是每次区间都严格缩小一半,最差情况是区间长度变为零(找不到的情况)

n => n 2 = > n 4 = > n 8 . . . 停 止 条 件 → n 2 x , 2 x ≥ n \large\frac{n}{2} => \large\frac{n}{4} =>\large\frac{n}{8} ... \overrightarrow{停止条件} \frac{n}{2^{x}} ,2^{x} \ge n 2n=>4n=>8n... 2xn,2xn

根据停止条件 2 x ≥ n 得 到 x = l o g   n 2^x \geq n得到 x = log\ n 2xnx=log n

算法运行次数为 x x x,推出时间复杂度为 O ( x ) = O ( l o g   n ) O(x) = O(log\ n) O(x)=O(log n)

二分查找变形

1. 求满足条件的最小值(后缀)

给定一个有单调性的数组 arr,问你大于等于 x x x的最小值是多少(不存在输出No)?

思路:

因为这是一个单调序列,那么满足大于等于 x x x条件的数一定是一个连续的区间,并且是从某一个位置一直往后(后缀),然后要找的就是这个后缀的最左边的值,就是大于等于 x x x的最小值。

假设当前的区间是 [ l , r ] [l,r] [l,r]

  1. x ≤ a r r [ m i d ] x \le arr[mid] xarr[mid],可行区间缩减成 [ l , m i d − 1 ] [l,mid-1] [l,mid1]
  2. x > a r r [ m i d ] x > arr[mid] x>arr[mid],则可行区间缩减成 [ m i d + 1 , r ] [mid+1,r] [mid+1,r]
  3. 二分的条件是 l < = r l <= r l<=r

代码

public static void find(int[] arr, int x) {
    
    
        int left = 0;
        int right = arr.length-1;
        int mid = 0;

        while (left <= right) {
    
    
            mid = ((right-left)>>1)+left;
            if (arr[mid] >= x) {
    
    
                right = mid-1;
            } else {
    
    
                left = mid+1;
            }
        }
        if (left  >= arr.length) {
    
    
            System.out.println("不存在大于等于"+x+"数");
        } else {
    
    
            System.out.println(arr[left]);
        }
    }

假设序列是 [ 1 , 2 , 4 , 7 , 9 ] [1,2,4,7,9] [1,2,4,7,9],我们要找大于等于 3的最小数字

在这里插入图片描述

结论:最后的 l l l一定停留在这个后缀的最左边的位置

2. 求满足条件的最大值(前缀)

给定一个有单调性的数组arr,问你小于等于 x x x的最大值是多少?

思路:

和上一题类似,这也是一个单调序列,小于等于 x x x的数也是一个连续的区间,这里是从某一个位置往前(前缀)的数字一定会小于等于x

假设区间是 [ l , r ] [l,r] [l,r]

  1. 如果 a r r [ m i d ] ≤ x , l = m i d + 1 arr[mid]\le x, l = mid+1 arr[mid]x,l=mid+1
  2. 如 果 a r r [ m i d ] > x , r = m i d − 1 如果arr[mid] > x, r = mid-1 arr[mid]>x,r=mid1
  3. 二分的循环条件 l ≤ r l \le r lr

代码

public static void find(int[] arr,int x) {
    
    
        int left = 0;
        int right = arr.length-1;
        int mid = 0;
        while (left <= right) {
    
    
            mid = ((right-left)>>1)+left;
            if (arr[mid] <= x) {
    
    
                left = mid + 1;
            } else {
    
    
                right = mid - 1;
            }
        }

        if (right < 0) {
    
    
            System.out.println("不存在小于等于"+x+"的数");
        } else {
    
    
            System.out.println(arr[right]);
        }
    }

假设序列是 [ 1 , 2 , 4 , 7 , 9 ] , 我 们 要 找 小 于 等 于 5 的 最 大 值 [1,2,4,7,9],我们要找小于等于5的最大值 [1,2,4,7,9],5

在这里插入图片描述

结论 :最后 r r r 一定停留在 前缀的最右边,也就是小于等于 x x x最大值

3. 求最短子序列

给定一个正整数序列,让你取一个子段,使得其区间的和大于等于 x x x,问你这个子段最短可能长度是多少。

例如:[1,2,4,7,9] ,给定 x = 13 时应该得到 2, x = 3 时应该得到1,x 大于 23时应该不存在

暴力代码

从 1、1+2、1+2+3、1+2…再到 2、2+4/2+4+7…枚举所有可能,当大于等于x时就break,因为前面的区间已经满足了条件,就是最短序列了没要继续往后

public static void shortestSubsequence(int[] arr, int x) {
    
    
    int min = Integer.MAX_VALUE;
    for (int i = 0; i < arr.length; i++) {
    
    
        int sum = 0;
        for (int j = i; j < arr.length; j++) {
    
    
            sum += arr[j];
            if (sum >= x) {
    
    
                min = Math.min(j-i+1,min);
                break;
            }
        }
    }
    if (min != Integer.MAX_VALUE) {
    
    
        System.out.println(min);
    } else {
    
    
        System.out.println("不存在");
    }
}

小结

1.发现单调性: 固定左端点后,右端点越远,则区间的和越大。

2.做法:枚举左端点,二分找最近的右端点使得其大于等于 x x x. 然后对所有左端点的答案取最小值。

3.预处理区间和:利用前缀和技巧。

4.复杂度: O ( n l o g n ) O(nlog n) O(nlogn)

4. 大于x的平方数

给定一个数 x x x,求解第一个大于 x x x平方数

二分思想,后缀模型

public static void maxFind(int x) {
    
    
        int left = 1;
        int right = x;
        int mid = 0;
        while (left <= right) {
    
    
            mid = ((right-left)>>1)+left;
            if (mid*mid > x) {
    
    
                right = mid-1;
            } else {
    
    
                left = mid+1;
            }
        }
        System.out.println(left*left);
    }

5.二分浮点数

给定一个数 x x x,求解 x \sqrt{x} x 的精确值,误差小于 1 e − 5 1e-5 1e5

技巧:在不好确定循环次数的时候,不妨可以固定循环的次数 T T T,只要 T ≥ l o g   n T \geq log\ n Tlog n 即可.

当外层循环次数越多,精确度越高,根据题目来

public static void sqrt(int x) {
    
    
        double left = 0;
        double right = x;
        double mid = 0;
        for (int i = 0; i < 55 ; i++) {
    
    
            mid = (right - left) / 2.0 + left;
            if (mid * mid <= x) {
    
    
                left = mid;
            } else {
    
    
                right = mid;
            }
        }
        System.out.println(left);
    }

二分答案

二分答案是二分的一种进阶思想,通过观察题目发现答案存在单调性,通过二分答案后检查答案是否符合要求

在这里插入图片描述

常规做法

  1. 发现答案存在单调性
  2. 二分答案
  3. 检查答案是否可行

引入题目

给定一个正整数序列,让你取一个子段,使得其区间的和大于等于 x x x,问你这个子段最短可能长度是多少。

前面做这到题目的时候是通过枚举长度,每次枚举 i i i 也就是长度,每次枚举后扫一遍序列,判断是否有一个长度为 i i i 的区间和 ≥ x \geq x x,如果存在,则记录结果然后 break

新思路

  1. 单调性:我们知道,区间越大,它的区间和越大,所以具有单调性。最终的答案满足一下关系

    在区间长度比较小的时候是不满足,在区间长度增加的过程中,知道某个分割点 S S S 使得它能够满足答案,这个位置就是我们希望找到的点,这个问题可以用二分解决

  2. 二分可能的长度 x x x

  3. 检查:对对于一个长度为 x x x的序列,如何检查这个答案可行?

    枚举所有可能的长度为x的字段(可以按右端点枚举),对他们的区间和之间取最大值,看是否大于等于 x x x

代码

/**
     * 判断是否存在一个长度为 len 的区间,它的和是否大于等于 x
     * @param array 序列
     * @param len 区间长度
     * @param x
     * @return
     */
    public static boolean flag(int[] array, int len, int x) {
    
    
        int max = 0;
        for (int i = len; i < array.length; i++) {
    
    
            // 枚举所有可能的区间,在它们的区间和之中取最大值,看看是否大于等于x
            max = Math.max(max,array[i]-array[i-len]);
        }

        // 判断和是否大于等于 x
        return max >= x;
    }
    public static void binarySequence(int[] arr,int x) {
    
    
         int[] array = new int[arr.length+1];
         //计算前缀和
        for (int i = 1; i < array.length; i++) {
    
    
            array[i] = array[i-1]+arr[i-1];
        }
//        System.out.println(Arrays.toString(arr));
//        System.out.println(Arrays.toString(array));

        //二分长度
        int left = 1;
        int right = arr.length;
        int mid = 0;
        while (left <= right) {
    
    
            mid = ((right-left)>>1)+left;
            if (flag(array,mid,x)) {
    
    
                right = mid-1;
            } else {
    
    
                left = mid+1;
            }
        }
        if (left >= array.length) {
    
    
            System.out.println("不存在序列和大于等于"+x);
        } else {
    
    
            System.out.println(left);
        }

    }

在这里插入图片描述

复杂度

n log ⁡ 2 n n \log_{2}{n} nlog2n

小结

直观来讲,二分答案就是一种(因为答案有单调性,所以)利用二分思想优化枚举答案过程的算法


猜你喜欢

转载自blog.csdn.net/weixin_53946852/article/details/124803730