爱喝冬瓜汤的萝卜

导航

二分搜索算法

二分搜索算法

有序向量的二分搜索算法能够有效的加快查找的速度.从无序的 O(n)变为 有序的O(logn)演化.

二分法的实现

基础版

  public int binarySearch(int[] nums, int target) {

        int lo = 0, hi = nums.length;
        while (lo < hi) {
            int mi = lo + (hi - lo) / 2;
            if (nums[mi] == target) {
                return mi;
            } else if (nums[mi] > target) {
                hi = mi - 1;
            } else {
                //nums[mi] < target
                hi = mi + 1;
            }
        }
        return -1;
    }

    上面这种算法是我们最常见二分搜索实现.这种搜索只能返回当前有序队列中是否包含了target元素,同时数组元素个数不同,返回的rank也有可能不一样.这种实现的功能比较简单.例如,当希望向有序数组中插入一个新元素的 target的时候,希望找出插入的位置的时候这个时候这种实现就不够好了.

对基础版的改进

    上面算法的实现中将有序数组分为两部分,逐步缩小空间,其中左右两边比较的次数不相同.转向左侧的比较有两次,转向右侧的比较有三次.而我们在计算mid的时候,是一分为二的.这里可以优化一下,在计算mid的时候偏向右边多点,这样使得左边区间多一些,右边区间少一些.这样就能够保证平衡.

语义清晰版本

     对于二分查找,我们需要找出一些位置,以便于我们插入,或者当系统中有多个等值的时候,我们需要知道最大的或者最小的哪个.这样的查询才会有意义.
如下图:

    在一个升序数组中,我们最希望确定的有四个rank.分别是第一个>= n,第一个>n,最后一个<n,最后一个 <= n的值.其中下面两个的rank可以通过上面两个的rank -1 获取到.
例如我们希望将target的元素插入到n后面,这个时候,这样我们可以通过二分查找找到最后一个>=n rank,然后插入到后面一个的位置上.

我们只实现上面的两个第一个>=n第一个>n 的rank.

第一个>=n的rank

public int binarySearchFirstGE(int[] nums, int target) {

        /**
         * 升序数组,找出 >= target的第一个的rank
         */
        // nums[0,lo) < target <= nums[hi,n)
        int lo = 0, hi = nums.length;
        while (lo < hi) {
            int mid = lo + (hi - lo) / 2;
            if (nums[mid] < target) {
                lo = mid + 1;
            } else {
                // nums[mid] >= target
                hi = mid;
            }
        }
        return lo;
    }

如其中的注释,我们可以看到其中的不变形的证明.

第一个 >n的rank

    public int binarySearchFirstG(int[] nums, int target) {

        /**
         * 升序数组,找出 > target的第一个的rank
         */
        // 不变性 nums[0,lo) <= target < nums[hi,n)   最终 结果 nums[lo == hi] = FirstG
        int lo = 0, hi = nums.length;
        while (lo < hi) {
            int mid = lo + (hi - lo) / 2;
            if (target < nums[mid]) {
                hi = mid;
            } else {
                // nums[mid] >= target
                lo = mid + 1;
            }
        }
        return lo;
    }

二分法使用范例

剑指offer 68题

有序数组中找出第一个>= target的元素的下标.
这个题的做法和标准做法一样.

 public int searchInsert(int[] nums, int target) {

        /**
         * 在升序数组中,找出 第一个>= target元素的位置.
         *  找出第一个 > target的rank
         */

        int lo = 0, hi = nums.length;
        while (lo < hi) {
            //不变量  nums[0,low) < target <= [high,n)
            int mid = lo + (hi - lo) / 2;
            if (nums[mid] < target) {
                // [lo,mid+1) < target
                lo = mid + 1;
            } else {
                //  target<= nums[mid]  target <=  [mid,high)
                hi = mid;
            }
        }
        return lo;
    }

剑指offer 8题

和大于等于 target 的最短子数组

 public int minSubArrayLen(int target, int[] nums) {

        if (nums == null || nums.length == 0) {
            return 0;
        }

        /**
         * 找出 连续子数组的 的长度 >= target的最短的长度
         * 使用 一个 数组记录以及记录前序和.
         * 然后 记录 [hi - lo ]之间的差 其中 <= target的结果.
         */

        int[] sums = new int[nums.length + 1];
        for (int i = 1; i < sums.length; i++) {
            sums[i] = sums[i - 1] + nums[i - 1];
        }

        int minLength = sums.length;
        for (int i = 0; i < sums.length - 1; i++) {
            int rank = binarySearch(sums, i + 1, sums.length, sums[i] + target);
            if (rank >= sums.length) {
                break;
            }
            minLength = Math.min(minLength, (rank - i));
        }
        return minLength == sums.length ? 0 : minLength;
    }

    /**
     * @param nums
     * @param lo
     * @param hi
     * @param target
     * @return
     */
    private int binarySearch(int[] nums, int lo, int hi, int target) {

        /**
         * 找出 第一个 >= target元素的下表
         * [0,low) < target <= [high,n)
         */

        int left = lo, right = hi;
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] < target) {
                //[lo,mid+1)< target
                left = mid + 1;
            } else {
                //nums[mid] >= target  target <=  [mid,high)
                right = mid;
            }
        }
        return right;
    }

这个题的实现的关键点:
1.使用前缀和的方式来计算子序列的和.其中第一个位置是0.
2.依次寻找第一个 >=(sums[i] + target)的元素的rank

剑指offer 73题

求狒狒最慢的速度,在给定时间内能吃完香蕉.

 public int minEatingSpeed(int[] piles, int h) {

        /**
         * 猴子吃香蕉:
         *
         * 不变量[0,lo) < speed  <= [hi,n)  时间不够 < pipe <= 时间刚好多种多的
         * 找出第一个 speed >= cal(h)的.  speed 提升, 速度愉快.
         *   最终 找到第一个 speed >= target的速度.
         *
         */
        int maxSpeed = Arrays.stream(piles).max().getAsInt();
        int low = 1, high = maxSpeed + 1;

        while (low < high) {
            int speed = low + (high - low) / 2;
            int costTime = calTime(speed, piles);
            if (h < costTime) {
                // speed < target 速度慢了.
                low = speed + 1;

            } else {
                //speed >= target 速度快了.
                high = speed;
            }
        }
        return low;
    }

    private int calTime(int speed, int[] piles) {


        int h = 0;
        for (int i = 0; i < piles.length; i++) {
            h += piles[i] / speed;
            if (piles[i] % speed != 0) {
                h++;
            }
        }
        return h;
    }

这个题是一个变种,最慢的速度是1,最快的速度是max(piles[i]).其中速度越大,消耗的时间越短.求出最小的speed 使得 hour>= target.
这个也可以用二分法来求解.

剑指offer 71题 按权重生成数

public class Solution {

    /**
     * 给定 一个 [1,3]
     */

    private int total = -1;
    private int[] pre = null;
    public Solution(int[] w) {

        pre = new int[w.length];
        pre[0] = w[0];
        total = w[0];
        for (int i = 1; i < pre.length; i++) {
            pre[i] = pre[i - 1] + w[i];
            total += w[i];
        }
    }

    /**
     * [1,4]
     *
     * @return
     */
    public int pickIndex() {

        int picRank = (int) (Math.random() * this.total) + 1;
        int rank = binarySearch(pre, picRank);
        return rank;
    }

    /**
     * 找出 nums中第一个 >= target的rank
     *
     * @param nums
     * @param target
     * @return
     */
    private int binarySearch(int[] nums, int target) {
        /**
         *  [0,lo) < target <= [hi,n)
         *  lo = hi ==> >= target
         */
        int lo = 0, hi = nums.length;
        while (lo < hi) {
            int mid = lo + (hi - lo) / 2;
            if (target <= nums[mid]) {
                hi = mid;
            } else {
                //target > nums[mid]
                lo = mid + 1;
            }
        }
        return lo;
    }
}

安权重分配随机数的方式:
首先记录下权重,然后使用前缀和.统计出每个数的位置.
然后生成一个随机数.以加权结果为base.
在前缀和数组中找出第一个 >= 随机数的下表rank.这个rank就是被选中的哪个值.

剑指offer 72 求一个数的平方根的题

如果有小数,将小数抹掉.例如:输入8,返回2.

  public int mySqrt(int x) {

        /**
         * 求整数的平方根.
         * 这里的实现方式是: 一个数是从 [0,x] 之间.
         * 元素越大 ,平方越大.
         * 求 [0,x]之间 ,最大的 i*i <= x  ==> 求 最小的 k*k > x 值. 然后 返回 k-1
         *
         * 区间是 [0,lo) <= target < [hi,n)
         * 最终 hi= lo为 > x的平方根 最小的值
         * 其中可能会出现 上溢出的问题,这里需要讲
         */

        long lo = 0;
        long  hi = (long)x + 1;
        while (lo < hi) {

            long mid = lo + (hi - lo) / 2;
            if ((long)mid * mid <=(long)x) {

                lo = mid + 1;
            } else {

                // (mid * mid) > x
                hi = mid;
            }
        }
        return (int)(lo - 1);
    }

题目:这个也可以用一个二分查找来求解.
一个数组[0,x+1)当i越大的时候,i*i越大.求最大的i,使得i*i>=x.
同时这个x可能会出现 越界.这里需要将所有的结果 适用 long来记录i的值.防止溢出.

剑指offer 69题.

求山峰型数组,其中山峰的位置.

    public int peakIndexInMountainArray(int[] arr) {

        /**
         * 因为是一个山峰的数组所以:会出现两种情况
         */
        int left = 1, right = arr.length - 2;
        int result = 0;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (arr[mid] > arr[mid + 1]) {
                result = mid;
                //这里是下山
                right = mid - 1;
            } else {
                //这里是上山
                left = mid + 1;
            }
        }
        return result;
    }

这个题使用二分法:使用当前元素和后一个元素进行比较.如果是升序,说明在右侧.否则在左侧.

posted on 2022-06-26 18:38  爱喝冬瓜汤的萝卜  阅读(18)  评论(0编辑  收藏  举报