Post

LeetCode - 二分查找技巧

算法题中的二分查找技巧往往是作为一种通用的技巧出现,毕竟其本质就是让每次search之后能筛掉一半的数据。

一个小坑: 二分边界搜索 并不等同于 寻找最接近的元素

658. 找到 K 个最接近的元素

找到最接近的元素后,以该元素为中心,双指针扩散添加元素直至有k个之后即可。

思路说起来很简单,但注意实现的一些细节。

例如找到接近的元素这一操作的实现,如果元素本身就存在,那么直接二分查找到左边界即可。
但是target可能不存在,所以还需要打一个补丁,因为有时候可能会错过那个最接近的元素。
例如arr = [0,0,1,2,3,3,4,7,7,8],target = 5,此时二分边界搜索找到的是arr[7]元素7,但是arr[6]元素4更接近5
这是因为最后一次条件判断时,left = 6 right = 7 mid = 6 ,arr[mid] < target,所以left = mid + 1 = 7,退出循环

最终findClose打完补丁后是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private int findClosest(int[] arr, int x) {
    int left = 0, right = arr.length;
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (arr[mid] >= x) {
            right = mid;
        } else left = mid + 1;
    }
    if (left == arr.length) left--; //极端情况下,left会不断右移直至越界,此时需要将left减一
    // 在理想情况下,即数组中存在x时,返回下标最小的x的下标
    if (arr[left] == x) return left;
    // 但x可能不存在于数组中,此时需要进一步判断,找到最接近x的数
    // 由于算法设计缺陷,可能会跳过最接近x的数,因此需要进一步判断
    int diffLeft = left > 0 ? Math.abs(arr[left - 1] - x) : Integer.MAX_VALUE;
    int diff = left < arr.length ? Math.abs(arr[left] - x) : Integer.MAX_VALUE;
    // 返回与目标值差距最小的元素
    return diffLeft <= diff ? left - 1 : left;
}

还有一点就是双指针扩散的时候注意边界条件,双指针应该指向新的元素用于判定
一种做法是将 (left,right)作为开区间初始化并扩散.
但是本案例中因为已经找到了最接近的元素,所以可以将 [left,right)作为左闭右开区间初始化并扩散.
在左边右开区间中 ,right - left 就是区间个数.
在两边均开的区间中, right - left + 1 才是区间个数.

二维中的二分查找

74. 搜索二维矩阵

主要工作是将二维表示的坐标mapping为index,例如 (i, j) 可以映射成一维的 index = i * n + j

实现 get(int[][] matrix, int index)函数后就可以直接根据index做二分查找了

240. 搜索二维矩阵 II

洞见是对于二维矩阵来说,搜索操作的起点不固定为左上角,

二分查找的核心 - 快速筛选元素

剑指 Offer 53 - I. 在排序数组中查找数字 I

统计一个数字在排序数组中出现的次数。

使用bs找左边界和右边界之后,右减左+1就是res

剑指 Offer 53 - II. 0~n-1中缺失的数字

常规的二分搜索让你在 nums 中搜索目标值 target,但这道题没有给你一个显式的 target,怎么办呢?

其实,二分搜索的关键在于,你是否能够找到一些规律,能够在搜索区间中一次排除掉一半。这道题的关键是,你是否能够找到一些规律,能够判断缺失的元素在哪一边?

洞见就是 nums[mid]mid 的关系,如果 nums[mid]mid 相等,则缺失的元素在右半边,如果 nums[mid]mid 不相等,则缺失的元素在左半边。

峰值元素

162. 寻找峰值

寻找峰值可以不用考虑 leftright,单纯考虑 mid 周边的情况。

如果走势下行(nums[mid] > nums[mid+1]),说明 mid 本身就是峰值或其左侧有一个峰值,所以需要收缩右边界(right = mid);

如果走势上行(nums[mid] < nums[mid+1]),则说明 mid 右侧有一个峰值,需要收缩左边界(left = mid + 1)。

因为题目说了 nums 中不存在相等的相邻元素,所以不用考虑 nums[mid] == nums[mid+1] 的情况

852. 山脉数组的峰顶索引

与162类似,就是找Max峰值

一些较为复杂的确认边界题型

后续的题目,可以先画图,然后判断如何更新搜索范围就很直观了

33. 搜索旋转排序数组

题目明确要对数时间复杂度,所以必须要bf,难点在于mid可能落在不同的地方需要分类讨论。

81. 搜索旋转排序数组 II

复杂的场景题 - 二分查找的一般套路

有些具体的场景题目可能一开始看不出来要用二分查找,比如让你求吃香蕉的「最小速度」,让你求轮船的「最低运载能力」,但其实求最值的过程就是搜索一个边界的过程,此时就可以使用二分查找技巧。

一般来说,要从题目中抽象出一个自变量 x,一个关于 x 的函数 f(x),以及一个目标值 target。同时,x, f(x), target 还要满足以下条件:

1、f(x) 必须是在 x 上的单调函数(单调增单调减都可以)

2、题目是让你计算满足约束条件 f(x) == target 时的 x 的值

如此便可判断可以使用二分查找的套路进行解决。

剩下的就是判断left 和right的初始化,以及找的是左边界还是右边界了

L875 爱吃香蕉的珂珂

定义速度为 x 时,需要 f(x) 小时吃完所有香蕉。题目便转换为 在f(x) == target的x左边界

x作为二分查找的区间,其边界就是left和right的初始值,在本题中 left =0 right= 最多的一坨香蕉个数

L1011 在 D 天内送达包裹的能力

类似的,定义运载能力为 x 时,需要 f(x) 天完成。题目便转换为 在f(x) == target的x左边界

运载能力最小为weight中的最大值,最大为所有weight的总值。

L410 分割数组的最大值

仔细观察后,其实是和L1011是一模一样的题目 maxGroupCaptity越大,groupNumber越小

f(maxGroupCaptity) = m 的左边界,maxGroupCaptity的min是nums中的最大元素,max是所有元素和。

This post is licensed under CC BY 4.0 by the author.