二分搜索--亿点点小变化和小技巧

20 阅读5分钟

引言

《二分搜索--魔鬼藏在细节中》总结了二分搜索最常见的易错点和细节,掌握了这些,会有种二分搜索也就这样的错觉。信心满满打开LC,依然是熟悉的一学就会,一写就废。
本文会找一些二分搜索的典型例题,结合这些题目的题解,聊聊二分搜索的亿点点小变化和小技巧。

拿捏.gif

Talk is cheap, show me the code

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

题目链接

本题是在做了最标准的二分题目之后,最基础的一个变化。与标准二分的差异点在于:标准二分在找到target只出现1次,找到就可以返回;本题中的数组,target可以出现多次,需要找到target的左右边界。
为了更加易于理解,我们把左右边界分开来看,先看如何二分求出左边界。除了在标准的二分写法之外,我们使用一个额外的变量ans,初始化为-1(可以初始化其他值,不要使用[0, len-1]中的值,这么做是为了这个初始值要易于判断是否在数组中可以找到左边界)。当我们找到target时,先把当前的mid赋值给ans,作为备选答案,然后继续去搜索左半区间,因为左半区间中可能还会存在target,这样,左半区间中的target才是真正的左边界。

/**
 * 搜索左边界
 * @param nums 原始数组
 * @param target 目标值
 * @return 目标值出现的左边界下标,如果不存在,返回-1
 */
private int searchLeft(int[] nums, int target) {
    int ans = -1, left = 0, right = nums.length;
    while (left < right) {
        int mid = left + ((right - left) >> 1);
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] == target) {
            ans = mid;
            right = mid;
        } else {
            right = mid;
        }
    }
    return ans;
    }

可以搜索左边界后,对右边界的搜索可以如法炮制

/**
 * 搜索右边界
 * @param nums 原始数组
 * @param target 目标值
 * @return 目标值出现的右边界下标,如果不存在,返回-1
 */
private int searchRight(int[] nums, int target) {
    int ans = -1, left = 0, right = nums.length;
    while (left < right) {
        int mid = left + ((right - left) >> 1);
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] == target) {
            ans = mid;
            left = mid + 1;
        } else {
            right = mid;
        }
    }
    return ans;
}

当然,searchLeftsearchRight存在相当一部分的相同代码,我们可以通过增加一个参数来表达要搜索的是左边界还是右边界,这里就不给出示例代码了。

本题最重要也是对其他题目有参考意义的技巧是:引入了一个额外的ans变量,在搜索的过程中,这个变量存储了当前最满足条件的答案,记录下这个结果,然后取继续搜索,如果有更好的答案就更新ans,最后再返回ans,这种思想,在后面很多二分搜索的变种题目中,都可以使用到。

两个数组间的距离值

题目链接

本题是在做了最标准的二分题目之后,另外一个常见的变化。如果arr2有序,那么如果arr2的下标[x]和下标[y]可以满足arr1[i]-arr2[j]| <= d,那么x和y之间的所有下标也都一定满足arr1[i]-arr2[j]| <= d,所以本题的本质也是求左边界和右边界。跟上一题的区别点在于,上一题是求target这个固定值的左右边界,本题是求满足一定条件的左右边界,多了一层条件的判断。

public int findTheDistanceValue(int[] arr1, int[] arr2, int d) {
    Arrays.sort(arr2);
    int ans = 0;
    for (int num : arr1) {
        if (!search(arr2, num - d, num + d)) {
            ans++;
        }
    }
    return ans;
}

/**
 * nums中是否有数字处于[min, max]之间
 * @param nums 数组
 * @param min 区间最小值
 * @param max 区间最大值
 * @return 数组中是否有数字处于[min, max]之间
 */
private boolean search(int[] nums, int min, int max) {
    int left = 0, right = nums.length;
    while (left < right) {
        int mid = left + ((right - left) >> 1);
        if (nums[mid] < min) {
            left = mid + 1;
        } else if (nums[mid] >= min && nums[mid] <= max) {
            return true;
        } else {
            right = mid;
        }
    }
    return false;
}

咒语和药水的成功对数

题目链接

本题的本质,依然是求出满足条件的区间。不过不同的是,前面2题都需要求出左右边界,而本题的题意中

一个咒语和药水的能量强度 相乘 如果 大于等于 success ,那么它们视为一对 成功 的组合。

也就是说,如果可以找到左边界,那么左边界右侧的值都是满足条件的区间。所以,本题只要求的左边界即可,比上面的题目还略微简单一些。

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

/**
 * 获取满足 current * potions[i] >= success 的数量
 * @param current 当前值
 * @param potions 从小到大排序后的数组
 * @param success 目标值
 * @return 满足 current * potions[i] >= success 的数量
 */
private int count(long current, int[] potions, long success) {
    int ans = -1, left = 0, right = potions.length;
    while (left < right) {
        int mid = left + ((right - left) >> 1);
        if (current * potions[mid] >= success) {
            ans = mid;
            right = mid;
        } else {
            left = mid + 1;
        }
    }
    if (ans < 0) {
        return 0;
    }
    return potions.length - ans;
}

总结

二分搜索的变化有很多,文本通过3道LC的例题和题解,解析了常见的二分搜索求区分的变化。在这类题目中,一个基本的技巧是引入了一个额外的ans变量,在搜索的过程中,这个变量存储了当前最满足条件的答案,记录下这个结果,然后取继续搜索,如果有更好的答案就更新ans,最后再返回ans。在求区间的过程中,把握好满足条件,能够快速将描述的条件转换成数学表达式,也是这些题目的主要解题技巧。

赞.png