赞
踩
小题考频:23
大题考频:4
难度:☆☆☆
将这些左括号依次压入栈中,最后入栈的左括号最先被弹出(匹配)
每当出现一个右括号,就“消耗”一个左括号
Eg1.
所有的左右括号都能匹配,匹配成功
Eg2.
扫描的右括号与栈顶的左括号不匹配,匹配失败
Eg3.
扫描到右括号但是栈已经空了——右括号单身,匹配失败
处理完所有括号但是栈非空——右括号单身,匹配失败
处理流程:
代码实现:
传入:
一个字符型数组 - 用于存放括号;数组长度
初始化一个栈
扫描:
如果是一个左括号,压入栈中;
如果是一个右括号,首先判断栈是否为空:
- 栈空则失败;
栈不空则栈顶元素出栈,并判断与所扫描元素是否匹配
检索完全部括号后,栈空说明匹配成功
——大家熟悉的算数表达式
由三个部分组成:操作数、运算符、界限符
界限符是必不可少的,反映了计算的先后顺序
Reference: Wikipedia
——Reverse Polish notation
一个灵感:可以不用界限符也能无歧义地表达运算顺序
Reverse Polish notation(逆波兰表达式=后缀表达式)
Polish notation(波兰表达式=前缀表达式)
后缀、前缀表达式的顺序中的操作数顺序不能变。
Eg.
中缀表达式:a + b - c,可以看做(a+b)-c
——用后缀表达式:(ab+)c- ,其中ab+看为整体;
当然!也能看做a + (b - c)
——用后缀表达式:a(bc-)+ ,其中bc-看为整体;
一个中缀表达式可能会转换成多种不一样的后缀、前缀表达式
中缀转后缀的手算方法:
①确定中缀表达式中各个运算符的运算顺序
②选择下一个运算符,按照「左操作数 -> 右操作数 -> 运算符」的方式组合成一个新的操作数
③如果还有运算符没被处理,就继续②
遵守左优先原则:
不遵守左优先原则:
后缀表达式
中运算符出现的顺序和中缀表达式
中的运算顺序是一样的客观来看两种都正确,只是“机算”结果是前者
算法的确定性:同样的输入只能得到同样的输出
Eg.
可以让最左边的加法先生效。
后缀表达式的手算方法:
从左往右扫描,每遇到一个运算符,就让运算符前面最近的两个操作数执行对应运算,合体为一个操作数
注意:两个操作数的左右顺序
Eg1.
Eg2.
特点:最后出现的操作数先被运算
== LIFO(后进先出)==
栈
!!!
用栈实现后缀表达式的计算:
①从左往右扫描下一个元素,直到处理完所有元素
②若扫描到操作数则压入栈,并回到①;否则执行③
③若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到①
Step1:
Step2:
Step3:
Step4:
Step5:
Step6:
Step7:
Step8:
注意:先出栈的是“右操作数”
后缀表达式适用于基于栈的编程语言(stack-oriented programming language),如:Forth、PostScript
中缀转前缀的手算方法:
①确定中缀表达式中各个运算符的运算顺序
②选择下一个运算符,按照「运算符 -> 左操作数 -> 右操作数」的方式组合成一个新的操作数
③如果还有运算符没被处理,就继续②
“右优先”原则:只要右边的运算符能先计算,就优先算右边的
Eg. 没有遵循右优先
Eg. 遵循右优先原则
练习:
中缀转后缀:“左优先”
中缀转前缀:“右优先”
用栈实现前缀表达式的计算:
①从右往左扫描下一个元素,直到处理完所有元素
②若扫描到操作数则压入栈,并回到①;否则执行③
③若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到①
注意:先出栈的是“左操作数”
初始化一个栈,用于保存暂时还不能确定运算顺序的运算符。
从左到右处理各个元素,直到末尾。可能遇到三种情况:
①遇到操作数。直接加入后缀表达式。
②遇到界限符。遇到“(”直接入栈;遇到“)”则依次弹出栈内运算符并加入后缀表达式,直到弹出“(”为止。注意:“(”不加入后缀表达式。
③遇到运算符。依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到“(”或栈空则停止。之后再把当前运算符入栈。
*
/
优先级高于+
-
按上述方法处理完所有字符后,将栈中剩余运算符依次弹出,并加入后缀表达式。
Step1:
操作数A,加入到后缀表达式中
Step2:
运算符 + ,栈空,入栈
Step3:
操作数B,加入到后缀表达式中
Step4:
运算符 - ,栈非空,依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,即弹出 + 号,再入栈 - 号
扫描到 - 号时,可以确定,他和 + 号中间夹住的操作数B,既要进行加法运算,也要进行减法运算,由于他们的优先级是相等的,那么可以根据左优先原则,可以让操作数B,先执行左边的运算符 + 运算,即弹出 + 号,让加法先生效
Step5:
操作数C,加入后缀表达式
Step6:
运算符 * ,此时栈顶元素是 - 法,优先级低于当前运算符,同时也不能直接运算乘法(如果后面有括号就不能确定能否先运行),先压入栈中
Step7:
操作数D,加入后缀表达式
Step8:
运算符 / ,此时栈顶元素是 * ,优先级等于当前运算符,弹出 * 加入后缀表达式;此时栈顶元素时 - ,优先级低于当前运算符,但 / 号无法判断,入栈 / 号
Step9:
操作数E,加入后缀表达式
Step10:
运算符 + ,此时栈顶元素是 / 号,优先级高于当前运算符,弹出 / 加入后缀表达式;此时栈顶元素是 - 号,优先级与当前运算符相等,弹出 - 号加入后缀表达式;入栈 + 号
Step11:
操作数F,加入后缀表达式
Step12:
处理完所有字符,将栈中剩余运算符依次弹出
Step1:
Step2:
Step3:
Step4:
Step5:
界限符 - 左括号“ ( ”,直接入栈
遇到界限符。遇到“(”直接入栈;遇到“)”则依次弹出栈内运算符并加入后缀表达式,直到弹出“(”为止。注意:“(”不加入后缀表达式。
Step6:
Step7:
运算符 - ,此时需要依次弹出栈中优先级更高或相等的所有运算符,碰到“ ( ”停止,入栈 - 号
碰到“ ( ”,即左边的操作数旁边有一个“ ( ”,此时不可以确定 - 号是否立即生效,确定不了运算顺序,压入栈中
Step8:
Step9:
界限符 - 右括号“ ) ”,依次弹出栈中的运算符,直到弹出“ ( ”为止
——左括号“ ( ”不加入后缀表达式
优先生效括号内的部分
Step10:
Step11:
Step12:
Step13:
Step14:
中缀转后缀 + 后缀表达式求值
——两个算法的结合
用栈实现中缀表达式的计算:
初始化两个栈,操作数栈和运算符栈
若扫描到操作数,压入操作数栈
若扫描到运算符或界限符,则按照“中缀转后缀”相同的逻辑压入运算符栈(期间也会弹出运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈顶元素并执行相应运算,运算结果再压回操作数栈)
操作数
;运算符
两个算法结合,设置两个栈,即可实现中缀表达式的计算
Step1:
操作数A,入栈
Step2:
运算符 + ,栈空,入栈
Step3:
操作数B,入栈
Step4:
运算符 - ,
① 栈顶 + 优先级相等,弹出 + 号,
② 并弹出两个栈顶操作数,
③ 进行运算后,压入操作数栈
类似“中缀转后缀”
Step5:
操作数C ,入栈
Step6:
运算符 * ,栈顶 - 优先级低于其,入栈 * 号
Step7:
操作数D ,入栈
Step8:
运算符 / ,栈顶 * 优先级相等,弹出 * ,弹出两个操作数,运算后压如操作数栈;
栈顶 - 低于其,入栈 / 号
Step9:
操作数E ,入栈
Step10:
运算符 + ,栈顶 / 优先级高于其,弹出 / ,弹出两个操作数,运算后压入操作数栈;
栈顶 - 优先级相等,弹出 - ,弹出两个操作数,运算后压入操作数栈
Step11:
操作数F ,入栈
Step12:
扫描完所有,将运算符栈剩余运算符弹出,并弹出对应的操作数进行计算
函数调用的特点:最后被调用的函数最先执行结束(LIFO)- 栈
函数调用时,需要用一个栈存储:
①调用返回地址
②实参
③局部变量
系统会创建函数调用栈,用来保存各个函数在调用过程中必须保存的信息。
比如:
- 运行main函数时,会把main函数中必要的信息压入栈中:局部变量a,b,c;
- main函数会调用func1函数,把func1执行结束之后,应该继续执行的代码的存储地址压入栈中(#1),函数调用的两个参数也会压入栈中(a,b),局部变量(x)也压入栈中;
——所以函数中修改a,b的值,修改的只是内存中func1的a,b的值,不会影响到main的a,b值
;- func1函数会调用func2函数,记录func2执行结束之后我们应该回到哪一句继续执行,把该句代码的存储地址(#2)压入栈中,调用的实参(x)和局部变量(m,n)也存入栈中;
- func2执行结束后,由栈顶的信息得知,应该往后执行#2句代码,然后把和func2相关的代码弹出栈,释放空间;
同样的,func1执行结束后,由栈顶信息得知,应该往后执行#1句代码,将func1相关信息删除,接着#1句代码继续执行。其实在main函数之前,还需要把某些不知道的信息压入栈底。
IDE中:
适合用“递归”算法解决:可以把原始问题转换为属性相同,但规模较小的问题
计算正整数的阶乘n!
把n!转换成n*(n-1)!,问题规模变小了,且属性相同
递归调用时,函数调用栈可称为“递归工作栈”
每进入一层递归,就将递归调用所需信息压入栈顶
每退出一层递归,就从栈顶弹出相应信息
问题规模逐渐收敛,直到n = 1,开始逐层返回到第一层的调用n = 10,那么factorial(n - 1)的调用返回值应该是9!,最后一层,再乘上n = 10,即10!返回给main函数的192行代码,将factorial(10)的值赋给x
缺点:太多层递归可能会导致栈溢出,内存资源是有限的
IDE中:
求斐波那契数列
传入4,在196行调用Fib3和Fib2;
首先调用Fib3(第2层#196,n=3),进入更深层递归,在196行调用Fib2和Fib1;
首先调用Fib2(第3层#196,n=2),进入更深层递归,在196行调用Fib1和Fib0;
首先调用Fib1(第4层#196,n=1),在194行return 1,弹出栈顶元素(第4层#196,n=1);
在Fib2层还需要调用Fib0(第4层#196,n=0),在192行return 0,弹出栈顶元素(第4层#196,n=0),Fib2相关的调用都有返回值了,Fib2可以向上一层返回(第3层#196,n=2);
……
……
Fib(2),Fib(1),Fib(0)被重复了多次,效率低
总结:
函数调用的特点:最后被调用的函数最先执行结束(LIFO)
函数调用时,需要用一个“函数调用栈”存储:
①调用返回地址
②实参
③局部变量
递归调用时,函数调用栈可称为“递归工作栈”
每进入一层递归,就将递归调用所需信息压入栈顶
每退出一层递归,就从栈顶弹出相应信息
缺点:效率低,太多层递归可能会导致栈溢出;可能包含很多重复计算
可以自定义栈将递归算法改造成非递归算法
注:在“树”章节中会详细学习
树是分层的,层次遍历:即一层一层的遍历,就需要队列的辅助。
新建一个队列,从根节点出发按层次遍历各个结点
Step1:遍历1号结点时,把1号结点的左右两个子节点2、3,都放到队列的队尾;
①->②->③
Step2:遍历完1号结点,出队;
②->③
Step3:遍历队头2号结点,把2号结点的左右两个子节点4、5,加入队尾;
②->③->④->⑤
Step4:遍历完2号结点,出队;
③->④->⑤
Step5:遍历队头3号结点,把3号结点的左右两个子节点6、7,加入队尾;
③->④->⑤->⑥->⑦
Step6:遍历完3号结点,出队;
④->⑤->⑥->⑦
Step7:遍历队头4号结点,其没有子节点,直接出队;
⑤->⑥->⑦
Step8:遍历队头5号结点,把5号结点的左右两个子节点8、9,加入队尾;
⑤->⑥->⑦->⑧->⑨
Step9:遍历完5号结点,出队;
⑥->⑦->⑧->⑨
Step10:遍历队头6号结点,其没有子节点,直接出队;
⑦->⑧->⑨
Step11:遍历队头7号结点,把7号结点的左右两个子节点10、11,加入队尾;
⑦->⑧->⑨->⑩->⑪
Step12:遍历完7号结点,出队;
⑧->⑨->⑩->⑪
Step13:⑧⑨⑩⑪号结点都没有子节点,直接出队
注:在“图”章节中会详细学习
实现思想和树类似
新建一个队列,每遍历一个结点,要检查和这个结点相邻的其他结点有没有被遍历过。
Step1:遍历1号结点,其相邻结点2、3都没有被遍历过,加入队尾,遍历完1号结点后让其出队
①->②->③
②->③
Step2:遍历2号结点,其相邻结点4没有被遍历过,加入队尾,遍历完2号结点后让其出队
②->③->④
③->④
Step3:遍历3号结点,其相邻结点5、6没有被遍历过,加入队尾,遍历完3号结点后让其出队
③->④->⑤->⑥
④->⑤->⑥
Step4:遍历4号结点,其相邻结点都被遍历过,直接出队
⑤->⑥
Step5:遍历5号结点,其相邻结点7、8没有被遍历过,加入队尾,遍历完5号结点后让其出队
⑤->⑥->⑦->⑧
⑥->⑦->⑧
Step6:⑥⑦⑧号结点的相邻结点都被遍历过,直接出队
多个进程争抢着使用有限的系统资源时,FCFS(First Come First Service,先来先服务)是一种常用策略。
FCFS:可用队列实现
——先申请的,先分配资源
Step1:多个进程排成就绪队列;
Step2:选择队头元素,上CPU执行一个短的时间片,迅速下CPU进入队尾;
Step3:重复Step2,选择队头元素,上CPU执行一个短的时间片,迅速下CPU进入队尾
这样所有进程都轮流得到了CPU处理。
场景:去学校打印店打印论文,多个同学用同一台打印机打印,打印的先后顺序如何?
系统开辟了一片缓冲区用于存放打印机此时暂时不能处理的数据,排成队列依次打印。
用栈实现括号匹配:
依次扫描所有字符,遇到左括号入栈,遇到右括号则弹出栈顶元素检查是否匹配。
匹配失败情况:
①左括号单身②右括号单身③左右括号不匹配
确定性
”函数调用的特点:最后被调用的函数最先执行结束(LIFO)
函数调用时,需要用一个“函数调用栈”存储:
①调用返回地址
②实参
③局部变量
递归调用时,函数调用栈可称为“递归工作栈”
每进入一层递归,就将递归调用所需信息压入栈顶
每退出一层递归,就从栈顶弹出相应信息
缺点:效率低,太多层递归可能会导致栈溢出;可能包含很多重复计算
可以自定义栈将递归算法改造成非递归算法
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。