记住这两个二分模板,秒杀所有二分查找算法题!

简介: 二分查找是一种在每次比较之后将查找空间一分为二的算法。当我们要处理的问题具备单调性,或者要搜寻序列的边界时,应该考虑使用二分查找算法

本文总结自 Doocs 开源社区旗下“leetcode”项目,作者杨立滨。


项目目前 Stars 数近 7k,将会持续更新,欢迎 Star 关注。


项目地址:https://github.com/doocs/leetcode


二分查找是一种在每次比较之后将查找空间一分为二的算法。当我们要处理的问题具备单调性,或者要搜寻序列的边界时,应该考虑使用二分查找算法。


但是,网上对于二分查找的描述纷繁复杂,包括提到二分查找很多的所谓变种写法,很容易让读者摸不清头脑。


其实,二分查找的解题套路可以简单总结为以下两个模板


模板 1:


boolean check(int x) {}int search(int left, int right) {    while (left < right) {        int mid = (left + right) >> 1;        if (check(mid)) {            right = mid;        } else {            left = mid + 1;        }    }    return left;}


模板 2:


boolean check(int x) {}int search(int left, int right) {    while (left < right) {        int mid = (left + right + 1) >> 1;        if (check(mid)) {            left = mid;        } else {            right = mid - 1;        }    }    return left;}


我们做二分题目时,可以按照以下步骤:


1.写出循环条件:while (left < right),注意是 left < right,而非 left <= right

2.循环体内,先无脑写出 mid = (left + right) >> 1

3.根据具体题目,实现 check() 函数(有时很简单的逻辑,可以不定义函数),想一下究竟要用 left = mid 还是 right = mid

4.如果 left = mid,那么无脑写出 else 语句 right = mid - 1,并且在 mid 计算时补充 +1,即 mid = (left + right + 1) >> 1

5.如果 right = mid,那么无脑写出 else 语句 left = mid,并且不需要更改 mid 的计算;

6.循环结束时,left 与 right 相等。


注意,这两个模板的优点是始终保持答案位于二分区间内,二分结束条件对应的值恰好在答案所处的位置。 对于可能无解的情况,只要判断二分结束后的 left 或者 right 是否满足题意即可。


接下来,我们来看几道二分题目,来实际应用一下这两个二分模板。


题目解析


题目 1:第一个错误的版本


你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。


假设你有 n 个版本 [1, 2, ..., n],你想找出导致之后所有版本出错的第一个错误的版本。


你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。


示例:


给定 n = 5,并且 version = 4 是第一个错误的版本。调用 isBadVersion(3) -> false调用 isBadVersion(5) -> true调用 isBadVersion(4) -> true所以,4 是第一个错误的版本。


解法:


此题我们应用模板 1 进行二分查找,找到第一个 BadVersion。


/* The isBadVersion API is defined in the parent class VersionControl.*/boolean isBadVersion(int version);public class Solution extends VersionControl {    public int firstBadVersion(int n) {        int left = 1, right = n;        while (left < right) {            int mid = (left + right) >>> 1;            if (isBadVersion(mid)) {                right = mid;            } else {                left = mid + 1;            }        }        return left;    }}


题目 2:在排序数组中查找数字


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


示例 1:


输入: nums = [5,7,7,8,8,10], target = 8输出: 2


示例 2:


输入: nums = [5,7,7,8,8,10], target = 6输出: 0


限制:


0 <= 数组长度 <= 50000


解法:


此题我们同时应用模板 1 和模板 2。两遍二分,分别查找出左边界和右边界。


class Solution {    public int search(int[] nums, int target) {        if (nums.length == 0) {            return 0;        }        // find first position        int left = 0, right = nums.length - 1;        while (left < right) {            int mid = (left + right) >>> 1;            if (nums[mid] >= target) {                right = mid;            } else {                left = mid + 1;            }        }        if (nums[left] != target) {            return 0;        }        int l = left;        // find last position        right = nums.length - 1;        while (left < right) {            int mid = (left + right + 1) >>> 1;            if (nums[mid] <= target) {                left = mid;            } else {                right = mid - 1;            }        }        return left - l + 1;    }}


题目 3:爱吃香蕉的珂珂


珂珂喜欢吃香蕉。这里有 N 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 H 小时后回来。


珂珂可以决定她吃香蕉的速度 K (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 K 根。如果这堆香蕉少于 K 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。


珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。


返回她可以在 H 小时内吃掉所有香蕉的最小速度 KK 为整数)。


示例 1:


输入: piles = [3,6,7,11], H = 8输出: 4


示例 2:


输入: piles = [30,11,23,4,20], H = 5输出: 30


示例 3:


输入: piles = [30,11,23,4,20], H = 6输出: 23


解法:


注:一般对于求最值/极值问题,可以考虑使用二分枚举,找到值的边界点。


对于本题,我们应用模板 1,二分枚举吃香蕉的速度,返回能在 H 小时内吃完所有香蕉的最小速度即可。


class Solution {    public int minEatingSpeed(int[] piles, int h) {        int mx = 0;        for (int pile : piles) {            mx = Math.max(mx, pile);        }        int left = 1, right = mx;        while (left < right) {            int mid = (left + right) >>> 1;            int s = 0;            for (int pile : piles) {                s += (pile + mid - 1) / mid;            }            if (s <= h) {                right = mid;            } else {                left = mid + 1;            }        }        return left;    }}


题目 4:在 D 天内送达包裹的能力


传送带上的包裹必须在 D 天内从一个港口运送到另一个港口。


传送带上的第 i 个包裹的重量为 weights[i]。每一天,我们都会按给出重量的顺序往传送带上装载包裹。我们装载的重量不会超过船的最大运载重量。


返回能在 D 天内将传送带上的所有包裹送达的船的最低运载能力。


示例 1:


输入:weights = [1,2,3,4,5,6,7,8,9,10], D = 5输出:15解释:船舶最低载重 15 就能够在 5 天内送达所有包裹,如下所示:第 1 天:1, 2, 3, 4, 5第 2 天:6, 7第 3 天:8第 4 天:9第 5 天:10请注意,货物必须按照给定的顺序装运,因此使用载重能力为 14 的船舶并将包装分成 (2, 3, 4, 5), (1, 6, 7), (8), (9), (10) 是不允许的。


示例 2:


输入:weights = [3,2,2,4,1,4], D = 3输出:6解释:船舶最低载重 6 就能够在 3 天内送达所有包裹,如下所示:第 1 天:3, 2第 2 天:2, 4第 3 天:1, 4


提示:


1 <= D <= weights.length <= 50000

1 <= weights[i] <= 500


解法:


注:一般对于求最值/极值问题,可以考虑使用二分枚举,找到值的边界点。


对于本题,我们应用模板 1,二分枚举运载能力 capacity,找到能在 D 天内送达的最小运载即可。


class Solution {    public int shipWithinDays(int[] weights, int days) {        int left = 1, right = Integer.MAX_VALUE;        while (left < right) {            int mid = (left + right) >> 1;            if (check(weights, days, mid)) {                right = mid;            } else {                left = mid + 1;            }        }        return left;    }    private boolean check(int[] weights, int days, int capacity) {        int cnt = 1, t = 0;        for (int w : weights) {            if (w > capacity) {                return false;            }            if (t + w <= capacity) {                t += w;            } else {                t = w;                ++cnt;            }        }        return cnt <= days;    }}


链接

目录
相关文章
|
3月前
|
算法
【算法】前缀和——二维前缀和模板题
【算法】前缀和——二维前缀和模板题
|
3月前
|
算法
【算法】二分查找——在排序数组中查找元素的第一个和最后一个位置
【算法】二分查找——在排序数组中查找元素的第一个和最后一个位置
|
1月前
|
算法 C# 索引
C#二分查找算法
C#二分查找算法
|
1月前
|
存储 算法 C语言
【C语言】二分查找算法
【C语言】二分查找算法
|
1月前
|
消息中间件 存储 算法
一文搞懂二分查找算法!
一文搞懂二分查找算法!
|
1月前
|
算法 Java 索引
数据结构与算法学习十五:常用查找算法介绍,线性排序、二分查找(折半查找)算法、差值查找算法、斐波那契(黄金分割法)查找算法
四种常用的查找算法:顺序查找、二分查找(折半查找)、插值查找和斐波那契查找,并提供了Java语言的实现代码和测试结果。
18 0
|
3月前
|
存储 算法 Java
深入算法基础二分查找数组
文章深入学习了二分查找算法的基础,通过实战例子详细解释了算法的逻辑流程,强调了确定合法搜索边界的重要性,并提供了Java语言的代码实现。
深入算法基础二分查找数组
|
3月前
|
算法
【算法】二分查找——二分查找
【算法】二分查找——二分查找
|
4月前
|
算法
【算法】二分查找(整数二分和浮点数二分)
算法学习——二分查找(整数二分和浮点数二分)
42 0
【算法】二分查找(整数二分和浮点数二分)
|
5月前
|
存储 算法 C语言
二分查找算法的概念、原理、效率以及使用C语言循环和数组的简单实现
二分查找算法的概念、原理、效率以及使用C语言循环和数组的简单实现