0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

二分搜索算法运用的框架套路

算法与数据结构 来源:labuladong 作者:labuladong 2021-08-25 16:06 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

我们前文 我作了首诗,保你闭着眼睛也能写对二分查找 详细介绍了二分搜索的细节问题,探讨了「搜索一个元素」,「搜索左侧边界」,「搜索右侧边界」这三个情况,教你如何写出正确无 bug 的二分搜索算法

但是前文总结的二分搜索代码框架仅仅局限于「在有序数组中搜索指定元素」这个基本场景,具体的算法问题没有这么直接,可能你都很难看出这个问题能够用到二分搜索。

对于二分搜索算法在具体问题中的运用,前文 二分搜索的运用(一) 和前文 二分搜索的运用(二) 有过介绍,但是还没有抽象出来一个具体的套路框架。

所以本文就来总结一套二分搜索算法运用的框架套路,帮你在遇到二分搜索算法相关的实际问题时,能够有条理地思考分析,步步为营,写出答案。

警告:本文略长略硬核,建议清醒时学习。

原始的二分搜索代码

二分搜索的原型就是在「有序数组」中搜索一个元素target,返回该元素对应的索引

如果该元素不存在,那可以返回一个什么特殊值,这种细节问题只要微调算法实现就可实现。

还有一个重要的问题,如果「有序数组」中存在多个target元素,那么这些元素肯定挨在一起,这里就涉及到算法应该返回最左侧的那个target元素的索引还是最右侧的那个target元素的索引,也就是所谓的「搜索左侧边界」和「搜索右侧边界」,这个也可以通过微调算法的代码来实现。

我们前文 二分搜索算法框架详解 详细探讨了上述问题,对这块还不清楚的读者建议复习前文,已经搞清楚基本二分搜索算法的读者可以继续看下去。

在具体的算法问题中,常用到的是「搜索左侧边界」和「搜索右侧边界」这两种场景,很少有让你单独「搜索一个元素」。

因为算法题一般都让你求最值,比如前文 二分搜索的运用(一) 中说的例题让你求吃香蕉的「最小速度」,让你求轮船的「最低运载能力」,前文 二分搜索的运用(二) 讲的题就更魔幻了,让你使每个子数组之和的「最大值最小」。

求最值的过程,必然是搜索一个边界的过程,所以后面我们就详细分析一下这两种搜索边界的二分算法代码。

「搜索左侧边界」的二分搜索算法的具体代码实现如下:

// 搜索左侧边界int left_bound(int[] nums, int target) {

if (nums.length == 0) return -1;

int left = 0, right = nums.length;

while (left 《 right) {

int mid = left + (right - left) / 2;

if (nums[mid] == target) {

// 当找到 target 时,收缩右侧边界

right = mid;

} else if (nums[mid] 《 target) {

left = mid + 1;

} else if (nums[mid] 》 target) {

right = mid;

}

}

return left;

}

假设输入的数组nums = [1,2,3,3,3,5,7],想搜索的元素target = 3,那么算法就会返回索引 2。

「搜索右侧边界」的二分搜索算法的具体代码实现如下:

// 搜索右侧边界int right_bound(int[] nums, int target) {

if (nums.length == 0) return -1;

int left = 0, right = nums.length;

while (left 《 right) {

int mid = left + (right - left) / 2;

if (nums[mid] == target) {

// 当找到 target 时,收缩左侧边界

left = mid + 1;

} else if (nums[mid] 《 target) {

left = mid + 1;

} else if (nums[mid] 》 target) {

right = mid;

}

}

return left - 1;

}

输入同上,那么算法就会返回索引 4:

好,上述内容都属于复习,我想读到这里的读者应该都能理解。记住上述的图像,所有能够抽象出上述图像的问题,都可以使用二分搜索解决。

二分搜索问题的泛化

什么问题可以运用二分搜索算法技巧?

首先,你要从题目中抽象出一个自变量x,一个关于x的函数f(x),以及一个目标值target。

同时,x, f(x), target还要满足以下条件:

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

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

上述规则听起来有点抽象,来举个具体的例子:

给你一个升序排列的有序数组nums以及一个目标元素target,请你计算target在数组中的索引位置,如果有多个目标元素,返回最小的索引。

这就是「搜索左侧边界」这个基本题型,解法代码之前都写了,但这里面x, f(x), target分别是什么呢?

我们可以把数组中元素的索引认为是自变量x,函数关系f(x)就可以这样设定:

// 函数 f(x) 是关于自变量 x 的单调递增函数// 入参 nums 是不会改变的,所以可以忽略,不算自变量int f(int x, int[] nums) {

return nums[x];

}

其实这个函数f就是在访问数组nums,因为题目给我们的数组nums是升序排列的,所以函数f(x)就是在x上单调递增的函数。

最后,题目让我们求什么来着?是不是让我们计算元素target的最左侧索引?

是不是就相当于在问我们「满足f(x) == target的x的最小值是多少」?

如果遇到一个算法问题,能够把它抽象成这幅图,就可以对它运用二分搜索算法。

算法代码如下:

// 函数 f 是关于自变量 x 的单调递增函数int f(int x, int[] nums) {

return nums[x];

}

int left_bound(int[] nums, int target) {

if (nums.length == 0) return -1;

int left = 0, right = nums.length;

while (left 《 right) {

int mid = left + (right - left) / 2;

if (f(mid, nums) == target) {

// 当找到 target 时,收缩右侧边界

right = mid;

} else if (f(mid, nums) 《 target) {

left = mid + 1;

} else if (f(mid, nums) 》 target) {

right = mid;

}

}

return left;

}

这段代码把之前的代码微调了一下,把直接访问nums[mid]套了一层函数f,其实就是多此一举,但是,这样能抽象出二分搜索思想在具体算法问题中的框架。

运用二分搜索的套路框架

想要运用二分搜索解决具体的算法问题,可以从以下代码框架着手思考:

// 函数 f 是关于自变量 x 的单调函数int f(int x) {

// ...

}

// 主函数,在 f(x) == target 的约束下求 x 的最值int solution(int[] nums, int target) {

if (nums.length == 0) return -1;

// 问自己:自变量 x 的最小值是多少?

int left = ...;

// 问自己:自变量 x 的最大值是多少?

int right = ... + 1;

while (left 《 right) {

int mid = left + (right - left) / 2;

if (f(mid) == target) {

// 问自己:题目是求左边界还是右边界?

// ...

} else if (f(mid) 《 target) {

// 问自己:怎么让 f(x) 大一点?

// ...

} else if (f(mid) 》 target) {

// 问自己:怎么让 f(x) 小一点?

// ...

}

}

return left;

}

具体来说,想要用二分搜索算法解决问题,分为以下几步:

1、确定x, f(x), target分别是什么,并写出函数f的代码。

2、找到x的取值范围作为二分搜索的搜索区间,初始化left和right变量。

3、根据题目的要求,确定应该使用搜索左侧还是搜索右侧的二分搜索算法,写出解法代码。

下面用几道例题来讲解这个流程。

例题一、珂珂吃香蕉

珂珂每小时最多只能吃一堆香蕉,如果吃不完的话留到下一小时再吃;如果吃完了这一堆还有胃口,也只会等到下一小时才会吃下一堆。

他想在警卫回来之前吃完所有香蕉,让我们确定吃香蕉的最小速度K。函数签名如下:

int minEatingSpeed(int[] piles, int H);

那么,对于这道题,如何运用刚才总结的套路,写出二分搜索解法代码?

按步骤思考即可:

1、确定x, f(x), target分别是什么,并写出函数f的代码。

自变量x是什么呢?回忆之前的函数图像,二分搜索的本质就是在搜索自变量。

所以,题目让求什么,就把什么设为自变量,珂珂吃香蕉的速度就是自变量x。

那么,在x上单调的函数关系f(x)是什么?

显然,吃香蕉的速度越快,吃完所有香蕉堆所需的时间就越少,速度和时间就是一个单调函数关系。

所以,f(x)函数就可以这样定义:

若吃香蕉的速度为x根/小时,则需要f(x)小时吃完所有香蕉。

代码实现如下:

// 定义:速度为 x 时,需要 f(x) 小时吃完所有香蕉// f(x) 随着 x 的增加单调递减int f(int[] piles, int x) {

int hours = 0;

for (int i = 0; i 《 piles.length; i++) {

hours += piles[i] / x;

if (piles[i] % x 》 0) {

hours++;

}

}

return hours;

}

target就很明显了,吃香蕉的时间限制H自然就是target,是对f(x)返回值的最大约束。

2、找到x的取值范围作为二分搜索的搜索区间,初始化left和right变量。

珂珂吃香蕉的速度最小是多少?多大是多少?

显然,最小速度应该是 1,最大速度是piles数组中元素的最大值,因为每小时最多吃一堆香蕉,胃口再大也白搭嘛。

这里可以有两种选择,要么你用一个 for 循环去遍历piles数组,计算最大值,要么你看题目给的约束,piles中的元素取值范围是多少,然后给right初始化一个取值范围之外的值。

我选择第二种,题目说了1 《= piles[i] 《= 10^9,那么我就可以确定二分搜索的区间边界:

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

int left = 1;

// 注意,right 是开区间,所以再加一

int right = 1000000000 + 1;

// ...

}

3、根据题目的要求,确定应该使用搜索左侧还是搜索右侧的二分搜索算法,写出解法代码。

现在我们确定了自变量x是吃香蕉的速度,f(x)是单调递减的函数,target就是吃香蕉的时间限制H,题目要我们计算最小速度,也就是x要尽可能小:

这就是搜索左侧边界的二分搜索嘛,不过注意f(x)是单调递减的,不要闭眼睛套框架,需要结合上图进行思考,写出代码:

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

int left = 1;

int right = 1000000000 + 1;

while (left 《 right) {

int mid = left + (right - left) / 2;

if (f(piles, mid) == H) {

// 搜索左侧边界,则需要收缩右侧边界

right = mid;

} else if (f(piles, mid) 《 H) {

// 需要让 f(x) 的返回值大一些

right = mid;

} else if (f(piles, mid) 》 H) {

// 需要让 f(x) 的返回值小一些

left = mid + 1;

}

}

return left;

}

PS:关于mid是否需要 + 1 的问题,前文 二分搜索算法详解 进行了详细分析,这里不展开了。

至此,这道题就解决了,现在可以把多余的 if 分支合并一下,最终代码如下:

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

int left = 1;

int right = 1000000000 + 1;

while (left 《 right) {

int mid = left + (right - left) / 2;

if (f(piles, mid) 《= H) {

right = mid;

} else {

left = mid + 1;

}

}

return left;

}

// f(x) 随着 x 的增加单调递减int f(int[] piles, int x) {

// 见上文

}

PS:我们代码框架中多余的 if 分支主要是帮助理解的,写出正确解法后建议合并多余的分支,可以提高算法运行的效率。

例题二、运送货物

再看看力扣第 1011 题「在 D 天内送达包裹的能力」:

要在D天内按顺序运输完所有货物,货物不可分割,如何确定运输的最小载重呢?

函数签名如下:

int shipWithinDays(int[] weights, int days);

和上一道题一样的,我们按照流程来就行:

1、确定x, f(x), target分别是什么,并写出函数f的代码。

题目问什么,什么就是自变量,也就是说船的运载能力就是自变量x。

运输天数和运载能力成反比,所以可以让f(x)计算x的运载能力下需要的运输天数,那么f(x)是单调递减的。

函数f(x)的实现如下:

// 定义:当运载能力为 x 时,需要 f(x) 天运完所有货物// f(x) 随着 x 的增加单调递减int f(int[] weights, int x) {

int days = 0;

for (int i = 0; i 《 weights.length; ) {

// 尽可能多装货物

int cap = x;

while (i 《 weights.length) {

if (cap 《 weights[i]) break;

else cap -= weights[i];

i++;

}

days++;

}

return days;

}

对于这道题,target显然就是运输天数D,我们要在f(x) == D的约束下,算出船的最小载重。

2、找到x的取值范围作为二分搜索的搜索区间,初始化left和right变量。

船的最小载重是多少?最大载重是多少?

显然,船的最小载重应该是weights数组中元素的最大值,因为每次至少得装一件货物走,不能说装不下嘛。

最大载重显然就是weights数组所有元素之和,也就是一次把所有货物都装走。

这样就确定了搜索区间[left, right):

public int shipWithinDays(int[] weights, int days) {

int left = 0;

// 注意,right 是开区间,所以额外加一

int right = 1;

for (int w : weights) {

left = Math.max(left, w);

right += w;

}

// ...

}

3、需要根据题目的要求,确定应该使用搜索左侧还是搜索右侧的二分搜索算法,写出解法代码。

现在我们确定了自变量x是船的载重能力,f(x)是单调递减的函数,target就是运输总天数限制D,题目要我们计算船的最小载重,也就是x要尽可能小:

这就是搜索左侧边界的二分搜索嘛,结合上图就可写出二分搜索代码:

public int shipWithinDays(int[] weights, int days) {

int left = 0;

// 注意,right 是开区间,所以额外加一

int right = 1;

for (int w : weights) {

left = Math.max(left, w);

right += w;

}

while (left 《 right) {

int mid = left + (right - left) / 2;

if (f(weights, mid) == days) {

// 搜索左侧边界,则需要收缩右侧边界

right = mid;

} else if (f(weights, mid) 《 days) {

// 需要让 f(x) 的返回值大一些

right = mid;

} else if (f(weights, mid) 》 days) {

// 需要让 f(x) 的返回值小一些

left = mid + 1;

}

}

return left;

}

到这里,这道题的解法也写出来了,我们合并一下多余的 if 分支,提高代码运行速度,最终代码如下:

public int shipWithinDays(int[] weights, int days) {

int left = 0;

int right = 1;

for (int w : weights) {

left = Math.max(left, w);

right += w;

}

while (left 《 right) {

int mid = left + (right - left) / 2;

if (f(weights, mid) 《= days) {

right = mid;

} else {

left = mid + 1;

}

}

return left;

}

int f(int[] weights, int x) {

// 见上文

}

本文就到这里,总结来说,如果发现题目中存在单调关系,就可以尝试使用二分搜索的思路来解决。搞清楚单调性和二分搜索的种类,通过分析和画图,就能够写出最终的代码。

责任编辑:haq

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 框架
    +关注

    关注

    0

    文章

    404

    浏览量

    18491
  • 搜索
    +关注

    关注

    0

    文章

    70

    浏览量

    17034
  • 代码
    +关注

    关注

    30

    文章

    4976

    浏览量

    74365

原文标题:我写了一个套路,助你随心所欲运用二分搜索

文章出处:【微信号:TheAlgorithm,微信公众号:算法与数据结构】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    京东商品搜索API技术实践指南

    {appSecret} + text{params} + text{appSecret}) $$ 、核心请求参数 参数名 类型 是否必填 说明 method string 是 API方法名,如:jd.union.open.goods.query keyword string 是
    的头像 发表于 03-19 17:09 378次阅读
    京东商品<b class='flag-5'>搜索</b>API技术实践指南

    淘宝按图搜索API技术实现详解

    ​ 一、接口基础说明 淘宝按图搜索API(商品图像搜索接口)允许开发者通过上传商品图片,获取淘宝平台上的相似商品列表。核心原理是通过图像特征提取与匹配算法实现,其技术流程如下: $$ begin
    的头像 发表于 03-13 16:43 212次阅读
    淘宝按图<b class='flag-5'>搜索</b>API技术实现详解

    淘宝图片搜索商品API指南

    一、摘要 淘宝图片搜索商品API是基于图像识别技术的智能搜索接口,允许用户通过上传商品图片来搜索相似或同款商品。该接口广泛应用于比价、找同款、商品识别等电商场景。 、接口概述 1.功
    的头像 发表于 12-08 14:26 1337次阅读

    线性搜索二分搜索介绍

    线性搜索(Linear Search):从数组的第一个元素开始,依次将当前元素与目标值进行比较,直到找到目标值或搜索完整个数组。 二分搜索(Binary Search):在有序数组中查
    发表于 12-01 07:36

    C语言的常见算法

    ) { return i; } } return -1; } ``` ### 二分查找 (Binary Search) ```c int binarySearch(int arr[], int l
    发表于 11-24 08:29

    京东拍立淘API开发指南:从零开始构建图像搜索应用

    京东图片识别搜索API(拍立淘)是基于深度学习的视觉搜索服务,通过卷积神经网络提取图像特征向量,结合近似最近邻搜索算法实现商品精准匹配‌。该技术解决了传统文字搜索难以描述商品外观的痛点
    的头像 发表于 11-09 17:40 2292次阅读

    模数转换器主要类型有哪些

    模数转换器采用逐步比较逼近策略,通过进制搜索算法将模拟信号转换为数字值。这类转换器在转换速度和精度之间实现了良好平衡,广泛应用于中等采样速率和中等精度的场景,如工业控制系统和医疗仪器。
    的头像 发表于 11-03 16:36 801次阅读

    苏宁搜索接口深析:全品类智能轨如何解决 O2O 电商的搜索痛点?

    本文深度解析苏宁全品类O2O搜索接口核心技术,涵盖智能轨引擎、库存联动系统与高并发架构设计,解决多品类参数识别、线上线下库存同步等电商搜索痛点,助力构建高效精准的现代电商搜索体系。
    的头像 发表于 10-28 16:20 985次阅读
    苏宁<b class='flag-5'>搜索</b>接口深析:全品类智能<b class='flag-5'>分</b>轨如何解决 O2O 电商的<b class='flag-5'>搜索</b>痛点?

    微店关键词搜索接口核心突破:动态权重算法与语义引擎的实战落地

    本文详解微店搜索接口从基础匹配到智能推荐的技术进阶路径,涵盖动态权重、语义理解与行为闭环三大创新,助力商家提升搜索转化率、商品曝光与用户留存,实现技术驱动的业绩增长。
    的头像 发表于 10-15 14:38 539次阅读

    用拼多多 API 实现拼多多店铺商品搜索权重提升

    将分步讲解如何利用 API 实现这一目标,确保内容真实可靠。 1. 理解搜索权重及其重要性 搜索权重是平台算法对商品排名的综合评分,基于多个因素计算。例如: 关键词相关性:商品标题和描述与用户
    的头像 发表于 08-19 17:23 1036次阅读
    用拼多多 API 实现拼多多店铺商品<b class='flag-5'>搜索</b>权重提升

    在薄膜框架上提供的 PIN 极管芯片 skyworksinc

    电子发烧友网为你提供()在薄膜框架上提供的 PIN 极管芯片相关产品参数、数据手册,更有在薄膜框架上提供的 PIN 极管芯片的引脚图、接线图、封装手册、中文资料、英文资料,在薄膜
    发表于 07-15 18:35
    在薄膜<b class='flag-5'>框架</b>上提供的 PIN <b class='flag-5'>二</b>极管芯片 skyworksinc

    肖特基极管四通道混频器芯片采用薄膜框架 skyworksinc

    电子发烧友网为你提供()肖特基极管四通道混频器芯片采用薄膜框架相关产品参数、数据手册,更有肖特基极管四通道混频器芯片采用薄膜框架的引脚图、接线图、封装手册、中文资料、英文资料,肖特
    发表于 07-15 18:33
    肖特基<b class='flag-5'>二</b>极管四通道混频器芯片采用薄膜<b class='flag-5'>框架</b> skyworksinc

    用一杯咖啡的时间,读懂AI二分类如何守护工业质量

    您是否想过,工厂里那些"非黑即白"的判断,正由AI用最简洁的逻辑守护质量?今天,让我们通过一个零件组装中的弹垫错装、漏装、多装、错序分类案例,拆解AI二分类技术的核心
    的头像 发表于 07-08 07:35 964次阅读
    用一杯咖啡的时间,读懂AI<b class='flag-5'>二分</b>类如何守护工业质量

    请问对SPDIF_Rx传来的48K,24Bit立体声信号作约160阶FIR电子二分频滤波器需怎样的MCU性能?

    请问对SPDIF_Rx 传来的48K,24Bit立体声信号作约160阶FIR电子二分频滤波器需怎样的MCU性能?
    发表于 04-29 07:00

    请问对SPDIF_Rx 传来的48K,24Bit立体声信号作约160阶FIR电子二分频滤波器需怎样的MCU性能?

    请问对SPDIF_Rx 传来的48K,24Bit立体声信号作约160阶FIR电子二分频滤波器需怎样的MCU性能?
    发表于 04-24 06:33