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

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

3天内不再提示

二分查找算法如何运用?

算法与数据结构 来源:labuladong 作者:labuladong 2020-10-30 09:39 次阅读

读完本文,你可以去力扣解决:410.分割数组的最大值(Hard)

经常有读者问我,读了之前的爆文二分查找框架详解之后,二分查找的算法他写的很溜了,但仅仅局限于在数组中搜索元素,不知道底怎么在算法题里面运用二分查找技巧来优化效率。

那我先说结论,你想用二分查找技巧优化算法,首先要把 for环形式的暴力算法写出来,如果算法中存在如下形式的 for 循环:

//func(i)是i的单调函数(递增递减都可以) intfunc(inti); //形如这种for循环可以用二分查找技巧优化效率 for(inti=0;i< n; i++) {     if (func(i) == target)         return i; }

如果func(i)函数是在i上单调的函数,一定可以使用二分查找技巧优化 for 循环。

「在i上单调的函数」是指func(i)的返回值随着i的增加而增加,或者随着i的增加而减小。

为什么满足这个条件就可以使用二分查找?因为这个逻辑和「在有序数组中查找一个元素」是完全一样的呀!

在有序数组nums中查找某一个数target,是不是最简单二分查找形式?我们看下普通的 for 循环遍历算法:

//nums是一个有序数组 int[]nums; //target是要搜索的元素 inttarget; //搜索target在nums中的索引 for(inti=0;i< nums.length; i++) {     if (nums[i] == target)         return i; }

既然nums是有序数组,你把nums[i]看做函数调用,是不是可以理解为nums在参数i上是单调的?这是不是和之前说的func(i)函数完全一样?

当然,前文二分查找框架详解说过,二分查找算法还有搜索左侧、右侧边界的变体,怎么运用到具体算法问题中呢?

还是注意观察 for 循环形式,只是不一定是func(i) == target作为终止条件,可能是<=或者>=的关系,这个可以根据具体的题目意思来推断,我们实操一下力扣第 410 题「分割数组的最大值」,难度Hard:

函数签名如下:

intsplitArray(int[]nums,intm);

这个题目有点类似前文一道经典动态规划题目高楼扔鸡蛋,题目比较绕,又是最大值又是最小值的。

简单说,给你输入一个数组nums和数字m,你要把nums分割成m个子数组。

肯定有不止一种分割方法,每种分割方法都会把nums分成m个子数组,这m个子数组中肯定有一个和最大的子数组对吧。

我们想要找一个分割方法,该方法分割出的最大子数组和是所有方法中最大子数组和最小的。

请你的算法返回这个分割方法对应的最大子数组和。

我滴妈呀,这个题目看了就觉得 Hard,完全没思路,这题怎么能和二分查找算法扯上关系?

说个小插曲,快手面试有一道画师画画的算法题,很难,就是以这道题为原型。当时我没做过这道力扣题,面试有点懵,不过之前文章二分查找算法运用写了两道类似的比较简单的题目,外加面试官的提示,把那道题做出来了。

面试做算法题的时候,题目一般都会要求算法的时间复杂度,如果你发现 O(NlogN) 这样存在对数的复杂度,一般都要往二分查找的方向上靠,这也算是个小套路。

言归正传,如何解决这道数组分割的问题?

首先,一个拍脑袋的思路就是用回溯算法框架暴力穷举呗,我简单说下思路:

你不是要我把nums分割成m个子数组,然后计算巴拉巴拉又是最大又是最小的那个最值吗?那我把所有分割方案都穷举出来,那个最值肯定可以算出来对吧?

怎么穷举呢?把nums分割成m个子数组,相当于在len(nums)个元素的序列中切m - 1刀,对于每两个元素之间的间隙,我们都有两种「选择」,切一刀,或者不切。

你看,这不就是标准的回溯暴力穷举思路嘛,我们根据穷举结果去计算每种方案的最大子数组和,肯定可以算出答案。

但是回溯的缺点就是复杂度很高,我们刚才说的思路其实就是「组合」嘛,时间复杂度就是组合公式:

时间复杂度其实是非常高的,所以回溯算法不是一个好的思路,还是得上二分查找技巧,反向思考这道题。

现在题目是固定了m的值,让我们确定一个最大子数组和;所谓反向思考就是说,我们可以反过来,限制一个最大子数组和max,来反推最大子数组和为max时,至少可以将nums分割成几个子数组。

比如说我们可以写这样一个split函数:

//在每个子数组和不超过max的条件下, //计算nums至少可以分割成几个子数组 intsplit(int[]nums,intmax);

比如说nums = [7,2,5,10],若限制max = 10,则split函数返回 3,即nums数组最少能分割成三个子数组,分别是[7,2],[5],[10]。

如果我们找到一个最小max值,满足split(nums, max)和m相等,那么这个max值不就是符合题意的「最小的最大子数组和」吗?

现在就简单了,我们只要对max进行穷举就行,那么最大子数组和max的取值范围是什么呢?

显然,子数组至少包含一个元素,至多包含整个数组,所以「最大」子数组和的取值范围就是闭区间[max(nums), sum(nums)],也就是最大元素值到整个数组和之间。

那么,我们就可以写出如下代码:

/*主函数,计算最大子数组和*/ intsplitArray(int[]nums,intm){ intlo=getMax(nums),hi=getSum(nums); for(intmax=lo;max<= hi; max++) {         // 如果最大子数组和是 max,         // 至少可以把 nums 分割成 n 个子数组         int n = split(nums, max);         // 为什么是 <= 不是 == ?         if (n <= m) {             return max;         }     }     return -1; } /* 辅助函数,若限制最大子数组和为 max, 计算 nums 至少可以被分割成几个子数组 */ int split(int[] nums, int max) {     // 至少可以分割的子数组数量     int count = 1;     // 记录每个子数组的元素和     int sum = 0;     for (int i = 0; i < nums.length; i++) {         if (sum + nums[i] >max){ //如果当前子数组和大于max限制 //则这个子数组不能再添加元素了 count++; sum=nums[i]; }else{ //当前子数组和还没达到max限制 //还可以添加元素 sum+=nums[i]; } } returncount; } //计算数组中的最大值 intgetMax(int[]nums){ intres=0; for(intn:nums) res=Math.max(n,res); returnres; } //计算数组元素和 intgetSum(int[]nums){ intres=0; for(intn:nums) res+=n; returnres; }

这段代码有两个关键问题:

1、对max变量的穷举是从lo到hi即从小到大的。

这是因为我们求的是「最大子数组和」的「最小值」,且split函数的返回值有单调性,所以从小到大遍历,第一个满足条件的值就是「最小值」。

2、函数返回的条件是n <= m,而不是n == m。按照之前的思路,应该n == m才对吧?

其实,split函数采用了贪心的策略,计算的是max限制下至少能够将nums分割成几个子数组。

举个例子,输入nums = [2,1,1], m = 3,显然分割方法只有一种,即每个元素都认为是一个子数组,最大子数组和为 2。

但是,我们的算法会在区间[2,4]穷举max,当max = 2时,split会算出nums至少可以被分割成n = 2个子数组[2]和[1,1]。

当max = 3时算出n = 2,当max = 4时算出n = 1,显然都是小于m = 3的。

所以我们不能用n == m而必须用n <= m来找到答案,因为如果你能把nums分割成 2 个子数组([2],[1,1]),那么肯定也可以分割成 3 个子数组([2],[1],[1])。

好了,现在 for 循环的暴力算法已经写完了,但是无法通过力扣的判题系统,会超时。

由于split是单调函数,且符合二分查找技巧进行优化的标志,所以可以试图改造成二分查找。

那么应该使用搜索左侧边界的二分查找,还是搜索右侧边界的二分查找呢?这个还是要看我们的算法逻辑:

intlo=getMax(nums),hi=getSum(nums); for(intmax=lo;max<= hi; max++) {     int n = split(nums, max);     if (n <= m) {         return max;     } }

可能存在多个max使得split(nums, max)算出相同的n,因为我们的算法会返回最小的那个max,所以应该使用搜索左侧边界的二分查找算法。

现在,问题变为:在闭区间[lo, hi]中搜索一个最小的max,使得split(nums, max)恰好等于m。

那么,我们就可以直接套用搜索左侧边界的二分搜索框架改写代码:

intsplitArray(int[]nums,intm){ //一般搜索区间是左开右闭的,所以hi要额外加一 intlo=getMax(nums),hi=getSum(nums)+1; while(lo< hi) {         int mid = lo + (hi - lo) / 2;         // 根据分割子数组的个数收缩搜索区间         int n = split(nums, mid);         if (n == m) {             // 收缩右边界,达到搜索左边界的目的             hi = mid;         } else if (n < m) {             // 最大子数组和上限高了,减小一些             hi = mid;         } else if (n >m){ //最大子数组和上限低了,增加一些 lo=mid+1; } } returnlo; } intsplit(int[]nums,intmax){/*见上文*/} intgetMax(int[]nums){/*见上文*/} intgetSum(int[]nums){/*见上文*/}

这段二分搜索的代码就是标准的搜索左侧边界的代码框架,如果不理解可以参见前文二分查找框架详解,这里就不展开了。

至此,这道题就通过二分查找技巧高效解决了。假设nums元素个数为N,元素和为S,则split函数的复杂度为O(N),二分查找的复杂度为O(logS),所以算法的总时间复杂度为O(N*logS)

责任编辑:lq

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

    关注

    23

    文章

    4593

    浏览量

    92546
  • 函数
    +关注

    关注

    3

    文章

    4290

    浏览量

    62346
  • 数组
    +关注

    关注

    1

    文章

    412

    浏览量

    25885

原文标题:二分查找算法如何运用?我和快手面试官进行了深入探讨…

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

收藏 人收藏

    评论

    相关推荐

    华纳云:Chord算法如何管理节点间的联系?

    finger表中查找最近的节点来实现。如果当前节点的finger表中没有直接指向目标节点的条目,它会将请求转发给finger表中指向的节点,直到找到目标节点。 动态操作和故障处理: Chord算法需要
    发表于 11-08 16:03

    直流接地故障的查找程序和方法

    流馈线;先拉储能、信号(公共屏)、测控,后拉保护、主变等装置;拉开保护装置电源后再上电前,需退相关保护装置出口压板。 查找步骤 判断接地性质 : 在直流系统出现接地的情况下,要先按照变电所绝缘监测装置的具体配置,通过
    的头像 发表于 10-08 10:26 242次阅读

    如何高效查找电气故障

    在现代工业生产中,电气系统的稳定运行对于确保生产安全和效率至关重要。然而,电气故障的发生往往不可避免,因此快速准确地查找并解决这些故障成为了电工和技术人员必备的技能。以下是一些高效查找电气故障的方法
    的头像 发表于 09-30 15:20 186次阅读

    怎样使用模拟电路实现信号的二分频呢?

    请问怎样使用模拟电路实现信号的二分频呢?
    发表于 09-10 08:06

    如何查找线路漏电的方法和步骤

    线路漏电是电气设备和线路中常见的故障之一,它不仅会导致设备损坏,还可能引发火灾等安全事故。因此,查找和处理线路漏电问题至关重要。 确定漏电类型 首先,我们需要确定漏电的类型。漏电分为两种:一种是接地
    的头像 发表于 08-26 09:07 1184次阅读

    器和耦合器的基本原理与应用

    输出端口的设备。它广泛应用于无线通信、雷达系统和射频测试设备中。 2. 功器的工作原理 功器通常由多个分支线组成,每个分支线的长度和特性阻抗都经过精确设计,以实现信号的均匀分配。常见的功器类型包括
    的头像 发表于 08-13 14:39 454次阅读

    明治案例 | 【AI二分类】剥蒜机大蒜方向识别

    ,就有了大蒜脱皮机,一钟轻轻松松剥1斤~而在这个设备上,必然也少不了明治传感其中的应用,本期小明就来分享一下~应用场景在自动剥蒜机中,需要设备精准判断蒜瓣的方向,
    的头像 发表于 07-16 08:25 187次阅读
    明治案例 | 【AI<b class='flag-5'>二分</b>类】剥蒜机大蒜方向识别

    次回路多点接地故障查找仪装置构成及原理——每日了解电力知识

    今天武汉摩恩智能电气有限公司带大家了解一下MEZN-6000 次回路多点接地故障查找仪。 MEZN-6000 次回路多点接地故障查找仪装置的构成: 本装置由分析仪、探测仪和信号采集
    的头像 发表于 06-11 10:12 325次阅读
    <b class='flag-5'>二</b>次回路多点接地故障<b class='flag-5'>查找</b>仪装置构成及原理——每日了解电力知识

    如何用C语言实现高效查找二分法)

    今天给分享一下使用C语言实现二分算法,主要包含以下几部分内容:二分查找算法介绍二分
    的头像 发表于 06-04 08:04 913次阅读
    如何用C语言实现高效<b class='flag-5'>查找</b>(<b class='flag-5'>二分</b>法)

    二分频电路总述 二分频电路的功能实现

    分频就是用同一个时钟信号通过一定的电路结构转变成不同频率的时钟信号。
    的头像 发表于 03-06 17:13 2015次阅读
    <b class='flag-5'>二分</b>频电路总述 <b class='flag-5'>二分</b>频电路的功能实现

    极管怎么正负

    极管怎么正负  正负极的分辨是指如何辨别极管的正负极。在电子电路中,极管是一种非常常见的电子元件,用于控制电流的流向。正确地分辨
    的头像 发表于 01-03 17:27 1947次阅读

    国产Apple Find My「查找」认证芯片-伦茨科技ST17H6x芯片

    深圳市伦茨科技有限公司(以下简称“伦茨科技”)发布ST17H6x Soc平台。成为继Nordic之后全球第家取得Apple Find My「查找」认证的芯片厂家,该平台提供可通过Apple
    的头像 发表于 12-12 10:37 553次阅读
    国产Apple Find My「<b class='flag-5'>查找</b>」认证芯片-伦茨科技ST17H6x芯片

    vlookup查找多个符合条件数值

    VLOOKUP是Excel中一种非常有用的函数,用于在指定的数据范围内查找特定值,并返回相应的结果。通常情况下,VLOOKUP只能找到第一个匹配的值并返回对应的结果。但是如果我们想要查找多个符合条件
    的头像 发表于 12-01 10:42 2299次阅读

    c语言在数组中查找指定元素

    的操作,本文将详细介绍C语言中如何在数组中进行查找,并提供几种常用的查找算法和技巧。 在开始之前,我们先来了解一下数组的基本概念和使用方法。数组由一系列相同类型的元素组成,这些元素存储在连续的内存单元中,可以通过
    的头像 发表于 11-24 10:07 3640次阅读

    驱动ADC第二部分 ADC驱动器与ADC匹配

    电子发烧友网站提供《差驱动ADC第二部分 ADC驱动器与ADC匹配.pdf》资料免费下载
    发表于 11-23 16:38 0次下载
    差<b class='flag-5'>分</b>驱动ADC第<b class='flag-5'>二部分</b> ADC驱动器与ADC匹配