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

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

3天内不再提示

分治算法详解:表达式的不同优先级

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

扫码添加小助手

加入工程师交流群

我们号已经写了 动态规划算法,回溯(DFS)算法,BFS 算法,贪心算法,双指针算法,滑动窗口算法,现在就差个分治算法没写了,今天来写一下,集齐七颗龙珠,就能召唤神龙了~

其实,我觉得回溯、分治和动态规划算法可以划为一类,因为它们都会涉及递归。

回溯算法就一种简单粗暴的算法技巧,说白了就是一个暴力穷举算法,比如让你 用回溯算法求子集、全排列、组合,你就穷举呗,就考你会不会漏掉或者多算某些情况。

动态规划是一类算法问题,肯定是让你求最值的。因为动态规划问题拥有 最优子结构,可以通过状态转移方程从小规模的子问题最优解推导出大规模问题的最优解。

分治算法呢,可以认为是一种算法思想,通过将原问题分解成小规模的子问题,然后根据子问题的结果构造出原问题的答案。这里有点类似动态规划,所以说运用分治算法也需要满足一些条件,你的原问题结果应该可以通过合并子问题结果来计算。

其实这几个算法之间界定并没有那么清晰,有时候回溯算法加个备忘录似乎就成动态规划了,而分治算法有时候也可以加备忘录进行剪枝。

我觉得吧,没必要过分纠结每个算法的定义,定义这东西无非文学词汇而已,反正能把题做出来你说这是啥算法都行,所以大家还是得多刷题,刷出感觉,各种算法都手到擒来。

最典型的分治算法就是归并排序了,核心逻辑如下:

voidsort(int[]nums,intlo,inthi){
intmid=(lo+hi)/2;
/******分******/
//对数组的两部分分别排序
sort(nums,lo,mid);
sort(nums,mid+1,hi);
/******治******/
//合并两个排好序的子数组
merge(nums,lo,mid,hi);
}

「对数组排序」是一个可以运用分治思想的算法问题,只要我先把数组的左半部分排序,再把右半部分排序,最后把两部分合并,不就是对整个数组排序了吗?

下面来看一道具体的算法题。

添加括号的所有方式

我来借力扣第 241 题讲讲什么是分治算法,先看看题目:

1aaef858-48d3-11eb-8b86-12bb97331649.png

简单说,就是给你输入一个算式,你可以给它随意加括号,请你穷举出所有可能的加括号方式,并计算出对应的结果

函数签名如下:

//计算所有加括号的结果
ListdiffWaysToCompute(Stringinput);

看到这道题的第一感觉肯定是复杂,我要穷举出所有可能的加括号方式,是不是还要考虑括号的合法性?是不是还要考虑计算的优先级?

是的,这些都要考虑,但是不需要我们来考虑。利用分治思想和递归函数,算法会帮我们考虑一切细节,也许这就是算法的魅力吧,哈哈哈。

废话不多说,解决本题的关键有两点:

1、不要思考整体,而是把目光聚焦局部,只看一个运算符。

这一点我们前文经常提及,比如 手把手刷二叉树第一期 就告诉你解决二叉树系列问题只要思考每个节点需要做什么,而不要思考整棵树需要做什么。

说白了,解决递归相关的算法问题,就是一个化整为零的过程,你必须瞄准一个小的突破口,然后把问题拆解,大而化小,利用递归函数来解决。

2、明确递归函数的定义是什么,相信并且利用好函数的定义。

这也是前文经常提到的一个点,因为递归函数要自己调用自己,你必须搞清楚函数到底能干嘛,才能正确进行递归调用。

下面来具体解释下这两个关键点怎么理解。

我们先举个例子,比如我给你输入这样一个算式:

1 + 2 * 3 - 4 * 5

请问,这个算式有几种加括号的方式?请在一秒之内回答我。

估计你回答不出来,因为括号可以嵌套,要穷举出来肯定得费点功夫。

不过呢,嵌套这个事情吧,我们人类来看是很头疼的,但对于算法来说嵌套括号不要太简单,一次递归就可以嵌套一层,一次搞不定大不了多递归几次。

所以,作为写算法的人类,我们只需要思考,如果不让括号嵌套(即只加一层括号),有几种加括号的方式?

还是上面的例子,显然我们有四种加括号方式:

(1) + (2 * 3 - 4 * 5)

(1 + 2) * (3 - 4 * 5)

(1 + 2 * 3) - (4 * 5)

(1 + 2 * 3 - 4) * (5)

发现规律了么?其实就是按照运算符进行分割,给每个运算符的左右两部分加括号,这就是之前说的第一个关键点,不要考虑整体,而是聚焦每个运算符。

现在单独说上面的第三种情况:

(1 + 2 * 3) - (4 * 5)

我们用减号-作为分隔,把原算式分解成两个算式1 + 2 * 34 * 5

分治分治,分而治之,这一步就是把原问题进行了「分」,我们现在要开始「治」了

1 + 2 * 3可以有两种加括号的方式,分别是:

(1) + (2 * 3) = 7

(1 + 2) * (3) = 9

或者我们可以写成这种形式:

1 + 2 * 3 = [9, 7]

4 * 5当然只有一种加括号方式,就是4 * 5 = [20]

然后呢,你能不能通过上述结果推导出(1 + 2 * 3) - (4 * 5)有几种加括号方式,或者说有几种不同的结果?

显然,可以推导出来(1 + 2 * 3) - (4 * 5)有两种结果,分别是:

9 - 20 = -11

7 - 20 = -13

那你可能要问了,1 + 2 * 3 = [9, 7]的结果是我们自己看出来的,如何让算法计算出来这个结果呢?

这个简单啊,再回头看下题目给出的函数签名:

//定义:计算算式 input 所有可能的运算结果
ListdiffWaysToCompute(Stringinput);

这个函数不就是干这个事儿的吗?这就是我们之前说的第二个关键点,明确函数的定义,相信并且利用这个函数定义

你甭管这个函数怎么做到的,你相信它能做到,然后用就行了,最后它就真的能做到了。

那么,对于(1 + 2 * 3) - (4 * 5)这个例子,我们的计算逻辑其实就是这段代码:

ListdiffWaysToCompute("(1+2*3)-(4*5)"){
Listres=newLinkedList<>();
/******分******/
Listleft=diffWaysToCompute("1+2*3");
Listright=diffWaysToCompute("4*5");
/******治******/
for(inta:left)
for(intb:right)
res.add(a-b);

returnres;
}

好,现在(1 + 2 * 3) - (4 * 5)这个例子是如何计算的,你应该完全理解了吧,那么回来看我们的原始问题。

原问题1 + 2 * 3 - 4 * 5是不是只有(1 + 2 * 3) - (4 * 5)这一种情况?是不是只能从减号-进行分割?

不是,每个运算符都可以把原问题分割成两个子问题,刚才已经列出了所有可能的分割方式:

(1) + (2 * 3 - 4 * 5)

(1 + 2) * (3 - 4 * 5)

(1 + 2 * 3) - (4 * 5)

(1 + 2 * 3 - 4) * (5)

所以,我们需要穷举上述的每一种情况,可以进一步细化一下解法代码:

ListdiffWaysToCompute(Stringinput){
Listres=newLinkedList<>();
for(inti=0;i< input.length(); i++) {
        charc=input.charAt(i);
//扫描算式input中的运算符
if(c=='-'||c=='*'||c=='+'){
/******分******/
//以运算符为中心,分割成两个字符串,分别递归计算
List
left=diffWaysToCompute(input.substring(0,i));
List
right=diffWaysToCompute(input.substring(i+1));
/******治******/
//通过子问题的结果,合成原问题的结果
for(inta:left)
for(intb:right)
if(c=='+')
res.add(a+b);
elseif(c=='-')
res.add(a-b);
elseif(c=='*')
res.add(a*b);
}
}
//basecase
//如果res为空,说明算式是一个数字,没有运算符
if(res.isEmpty()){
res.add(Integer.parseInt(input));
}
returnres;
}

有了刚才的铺垫,这段代码应该很好理解了吧,就是扫描输入的算式input,每当遇到运算符就进行分割,递归计算出结果后,根据运算符来合并结果。

这就是典型的分治思路,先「分」后「治」,先按照运算符将原问题拆解成多个子问题,然后通过子问题的结果来合成原问题的结果

当然,一个重点在这段代码:

//basecase
//如果res为空,说明算式是一个数字,没有运算符
if(res.isEmpty()){
res.add(Integer.parseInt(input));
}

递归函数必须有个 base case 用来结束递归,其实这段代码就是我们分治算法的 base case,代表着你「分」到什么时候可以开始「治」。

我们是按照运算符进行「分」的,一直这么分下去,什么时候是个头?显然,当算式中不存在运算符的时候就可以结束了。

那为什么以res.isEmpty()作为判断条件?因为当算式中不存在运算符的时候,就不会触发 if 语句,也就不会给res中添加任何元素。

至此,这道题的解法代码就写出来了,但是时间复杂度是多少呢?

如果单看代码,真的很难通过 for 循环的次数看出复杂度是多少,所以我们需要改变思路,本题在求所有可能的计算结果,不就相当于在求算式input的所有合法括号组合吗?

那么,对于一个算式,有多少种合法的括号组合呢?这就是著名的「卡特兰数」了,最终结果是一个组合数,推导过程稍有些复杂,我这里就不写了,有兴趣的读者可以自行搜索了解一下。

其实本题还有一个小的优化,可以进行递归剪枝,减少一些重复计算,比如说输入的算式如下:

1 + 1 + 1 + 1 + 1

那么按照算法逻辑,按照运算符进行分割,一定存在下面两种分割情况:

(1 + 1) + (1 + 1 + 1)

(1 + 1 + 1) + (1 + 1)

算法会依次递归每一种情况,其实就是冗余计算嘛,所以我们可以对解法代码稍作修改,加一个备忘录来避免这种重复计算:

//备忘录
HashMap>memo=newHashMap<>();

ListdiffWaysToCompute(Stringinput){
//避免重复计算
if(memo.containsKey(input)){
returnmemo.get(input);
}
/******其他都不变******/
Listres=newLinkedList<>();
for(inti=0;i< input.length(); i++) {
        //...
}
if(res.isEmpty()){
res.add(Integer.parseInt(input));
}
/***********************/

//将结果添加进备忘录
memo.put(input,res);
returnres;
}

当然,这个优化没有改变原始的复杂度,只是对一些特殊情况做了剪枝,提升了效率。

最后总结

解决上述算法题利用了分治思想,以每个运算符作为分割点,把复杂问题分解成小的子问题,递归求解子问题,然后再通过子问题的结果计算出原问题的结果。

把大规模的问题分解成小规模的问题递归求解,应该是计算机思维的精髓了吧,建议大家多练~

责任编辑:xj

原文标题:分治算法详解:表达式的不同优先级

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


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

    关注

    23

    文章

    4760

    浏览量

    97142
  • 分治法
    +关注

    关注

    0

    文章

    3

    浏览量

    5820

原文标题:分治算法详解:表达式的不同优先级

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

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

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    抢占优先级和子优先级

    关于抢占优先级和子优先级: 1)具有高抢占式优先级的中断可以在具有低抢占式优先级的中断服务程序执行过程中被响应,即中断嵌套,或者说高抢占式优先级
    发表于 12-03 07:11

    电能质量在线监测装置的暂态数据补传的优先级是怎样的?

    电能质量在线监测装置的暂态数据补传优先级设计遵循 事件驱动、主站指令优先、资源动态分配 的原则,结合行业标准和设备机制,形成以下多层级优先级体系: 一、最高优先级:紧急事件驱动补传 1
    的头像 发表于 11-06 14:02 93次阅读

    I1电流计算对不对?怎么推导不出来I1的表达式是图中那样

    I1电流计算对不对?怎么推导不出来I1的表达式是图中那样
    发表于 09-28 18:01

    优先级线程无法调度怎么解决?

    1,设置了3,5,6,8几个优先级,设备在现场正常运行了一年多后,显示、前端、后端这3个低优先级线程异常了,表现为屏幕不动,前端采集数据没有变化等,其他高优先级的线程如通讯,按键都能正常运行,通讯有喂狗操作,停止通讯,会看门狗复
    发表于 09-25 07:33

    什么是RTOS中的优先级反转

    当一个高优先级任务正在等待一个资源,但一个低优先级任务正在持有它,一个中等优先级任务继续在中间运行时,就会发生优先级反转——阻止低优先级任务
    的头像 发表于 09-09 14:50 772次阅读

    TLe9893怎么调整外设的中断优先级

    你好林工,我该怎么调整外设的中断优先级?是否可以通过工具调整?默认设置下,是不是Brdv的在中断优先级高于T20和can?
    发表于 08-01 06:20

    labview如何使用VISA串口资源查找的正则表达式提取串口的资源名称?

    如图,如何利用VISA资源查找的正则表达式从很多串口当中提取想要的目标串口(Quectel USB AT Port这个串口)?
    发表于 07-07 17:20

    Cubeide1.18.1在线调试改变\"现场表达式\"中的值提示找不到地址,为什么?

    Cubeide1.18.1在线调试时,在\"现场表达式\"中添加全局变量,然后改变其数值,Console窗口提示: Failed to read all registers
    发表于 06-12 06:50

    Cubeide1.18.1在线调试改变\"现场表达式\"中的值提示找不到地址是怎么回事?

    Cubeide1.18.1在线调试时,在\"现场表达式\"中添加全局变量,然后改变其数值,Console窗口提示: Failed to read all registers
    发表于 06-10 08:26

    干货分享 | 零基础上手!TSMaster图形信号表达式实操指南

    TSMaster软件支持在图形里面的信号表达式功能,主要用于多信号表达式运算和显示的场景。本文将以A2L中的标定变量为例,介绍如何使用图形中的信号表达式功能进行多信号的后处理运算和显示。本文关键词
    的头像 发表于 06-06 20:03 672次阅读
    干货分享 | 零基础上手!TSMaster图形信号<b class='flag-5'>表达式</b>实操指南

    Cubeide1.18.1在线调试改变\"现场表达式\"中的值提示找不到地址,怎么解决?

    Cubeide1.18.1在线调试时,在\"现场表达式\"中添加全局变量,然后改变其数值,Console窗口提示: Failed to read all registers
    发表于 06-06 08:27

    CyU3PDebugPrint的最高优先级和最低优先级是什么?

    [i]CyU3PDebugPrint的最高优先级和最低优先级是什么?
    发表于 05-13 08:22

    Cubeide1.18.1在线调试改变\"现场表达式\"中的值提示找不到地址怎么解决?

    Cubeide1.18.1在线调试时,在\"现场表达式\"中添加全局变量,然后改变其数值,Console窗口提示: Failed to read all registers
    发表于 04-27 06:18

    配电柜—断电危机?配电柜故障排查优先级指南

    在排查配电柜故障过程中,合理安排排查优先级至关重要。下面聊一下如何科学合理安排配电柜故障排查优先级顺序。
    的头像 发表于 03-06 18:55 809次阅读
    配电柜—断电危机?配电柜故障排查<b class='flag-5'>优先级</b>指南

    利用栈结构实现四则运算的巧妙方法

    ,非常符合人的正常思维,但是计算机计算的话不方便。 中缀表达式可以转换成后缀表达式,这种表达式看起来抽象一些,但是不需要括号或者优先级,计算机计算的话比较方便。 整个过程有点复杂,分的
    的头像 发表于 02-07 11:06 972次阅读