二分查找算法及相关题目

Posted Algorithms

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了二分查找算法及相关题目相关的知识,希望对你有一定的参考价值。

二分查找(折半查找)算法是一种比较简单的算法,依赖于数组的有序,因为效率高,实现简单而且使用场景多而广泛存在。

1. 寻找右区间

来自力扣题库【436】题:寻找右区间

1.1 题目描述

给你一个区间数组 intervals ,其中 intervals[i] = [starti, endi] ,且每个 都 不同 。

区间 i 的 右侧区间 可以记作区间 j ,并满足 ,且 最小化 。

返回一个由每个区间 i 的右侧区间的最小起始位置组成的数组。如果某个区间 i 不存在对应的右侧区间 ,则下标 i 处的值设为 -1

注意:

  • 每个间隔的起点都 不相同

1.2 示例数据

输入:intervals = [[1,2]]
输出:[-1]
解释:集合中只有一个区间,所以输出-1。
输入:intervals = [[3,4],[2,3],[1,2]]
输出:[-1, 0, 1]
解释:对于 [3,4] ,没有满足条件的“右侧”区间。
对于 [2,3] ,区间[3,4]具有最小的“右”起点;
对于 [1,2] ,区间[2,3]具有最小的“右”起点。
输入:intervals = [[1,4],[2,3],[3,4]]
输出:[-1, 2, -1]
解释:对于区间 [1,4] 和 [3,4] ,没有满足条件的“右侧”区间。
对于 [2,3] ,区间 [3,4] 有最小的“右”起点。

1.3 解题思路

使用二分查找算法,对每一个 start 的起始位置的下标进行记录,然后再对 start 进行递增排序,之所以递增排序,是为了方便在后面的查找过程中对每一个区间的 end 进行二分查找。

具体看注释吧。

class Solution {
    public int[] findRightInterval(int[][] intervals) {
        Map<Integer, Integer> mapper = new TreeMap<>();
        int n = intervals.length;
        // 对特殊情况进行处理
        if (n <= 1return new int[]{-1};
        // 然后把每一个 start 的位置 i 进行记录,并按照 start 进行递增排序
        for (int i = 0; i < n; ++i) mapper.put(intervals[i][0], i);
        // arr 用来保存mapper里面的 [start, idx] 数据对,之所以这么做,是为了方便二分查找
        int[][] arr = new int[mapper.size()][2];
        int idx = 0;
        for (Map.Entry<Integer, Integer> entry : mapper.entrySet()) {
            arr[idx][0] = entry.getKey();
            arr[idx++][1] = entry.getValue();
        }
        // 构建返回值
        int[] ret = new int[n];
        for (int i = 0; i < n; ++i) {
            // 对每一个 intervals[i][1](end) 位置,都去找下一个大于等于 end 的下标
            ret[i] = binarySearch(arr, intervals[i][1]);
        }
        return ret;
    }

    // 大于 target 的第一个位置
    private int binarySearch(int[][] nums, int target) {
        int left = 0, right = nums.length - 1;
        // 看看是不是所有的起始位置,都没有这个 target 终止位置大,是的话说明这个target就不存在下一个起始点
        if (nums[right][0] < target) return -1;
        // 二分查找
        while (left < right) {
            int mid = left + (right - left) / 2;
            // 如果找到了,就直接返回
            if (nums[mid][0] == target) return nums[mid][1];
            else if (nums[mid][0] < target) left = mid + 1;
            else right = mid;
        }
        // 最后 left 位置就是大于 target 的第一个位置
        return nums[left][1];
    }
}

2. 找出第K小的距离对

来自力扣题库【719】题:找出第K小的距离对

2.1 题目描述

给定一个整数数组,返回所有数对之间的第 k 个最小距离。一对 (A, B) 的距离被定义为 AB 之间的绝对差值。

注意:

  • 2 <= len(nums) <= 10000.

  • 0 <= nums[i] < 1000000.

  • 1 <= k <= len(nums) * (len(nums) - 1) / 2.

2.2 示例数据

输入:
nums = [1,3,1]
k = 1
输出:0 
解释:
所有数对如下:
(1,3) -> 2
(1,1) -> 0
(3,1) -> 2
因此第 1 个最小距离的数对是 (1,1),它们之间的距离为 0。

2.3 解题思路

这是一道困难题,不过使用二分查找的话思路还是挺简单的。

首先转换一下思路,对于所有数对的第 k 个最小距离 x,那么 x 实际上是一个阈值,对于这个阈值,需要有 k 个小于等于它。而所有数对的差值,是可以枚举出来的(实际上不需要全部枚举,因为使用二分查找方法每次都淘汰掉这次的遍历数组个数的一半)。

还依赖于一个现成的思路:对于按非递减排序好的数组 nums,那么对于差值不大于 midnums[i, j] 区间,就已经有 j - i 个了(一个数组的最大值和最小值的差都小于等于 mid,那么最大值和最小值之间的任意两个数字之间的差值都不会大于 mid

那么有了上面的初步思路以后,就可以首先对数组进行排序,然后就使用二分查找来完成最小距离的查找。

class Solution {
    public int smallestDistancePair(int[] nums, int k) {
        // 首先把数组进行排序,方便进行二分查找
        Arrays.sort(nums);
        // 这里的 low,指的是数组中两个数相差的最小值,high 是数组中两个值可能相差的最大值
        int low = 0, high = nums[nums.length - 1] - nums[0];
        while (low < high) {
            // 找到差值的中间值,并尝试以 mid 来看是不是差值小于等于 mid 的数对个数符合要求 k
            int mid = low + (high - low) / 2;
            // count 用来统计所有的符合要求的情况,left 指针用来标记循环遍历整个数组的左边界
            int count = 0, left = 0;
            for (int right = 0; right < nums.length; ++right) {
                // 在循环过程中,如果 nums[right] - nums[left] 大于了 mid,说明 left 太小了,这时候增大 left就可以使得数量减少
                while (nums[right] - nums[left] > mid) ++left;
                // 左右指针之间的数对都符合 nums[right] - nums[left] <= mid的要求
                count += right - left;
            }
            // 如果符合差值小于等于 mid 的数对个数太多了(count >= k) 那么就减小最高值
            if (count >= k) high = mid;
            // 否则说明数对个数太少了,需要加大阈值 mid
            else low = mid + 1;
        }
        // 到最后 low 就是最小的差值
        return low;
    }
}

3. 搜索旋转排序数组Ⅱ

来自力扣题库【81】:搜索旋转排序数组Ⅱ

3.1 题目描述

已知存在一个按非降序排列的整数数组 nums ,数组中的值不必互不相同。

在传递给函数之前,nums 在预先未知的某个下标 k0 <= k < nums.length)上进行了 旋转 ,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如,[0,1,2,4,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4]

给你 旋转后 的数组 nums 和一个整数 target ,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums中存在这个目标值 target ,则返回 true ,否则返回 false

注意:

  • 1 <= nums.length <= 5000
  • -104 <= nums[i] <= 104
  • 题目数据保证 nums 在预先未知的某个下标上进行了旋转
  • -104 <= target <= 104

3.2 示例数据

输入:nums = [2,5,6,0,0,1,2], target = 0
输出:true
输入:nums = [2,5,6,0,0,1,2], target = 3
输出:false

3.3 解题思路

两边各有序,本质上还是整体有序,所以使用二分搜索是比较棒的选择了,只不过需要在原来的二分搜索模板上加点东西。

这里有一个不同于上面题目的地方在于使用了递归方法来完成二分查找。

不过有一个潜在问题:如果数据正好反过来成了逆序呢,会不会递归栈溢出

以上是关于二分查找算法及相关题目的主要内容,如果未能解决你的问题,请参考以下文章

二分查找及对应的几道经典题目

『嗨威说』算法设计与分析 - 算法第二章上机实践报告(二分查找 / 改写二分搜索算法 / 两个有序序列的中位数)

python函数:递归函数及二分查找算法

查找算法之“二分查找”

C言语二分查找(折半查找)算法及代码

AK leetcode 流浪计划 - 二分查找