当前位置:   article > 正文

[入门必看]数据结构3.3:栈和队列的应用_栈和队列应用

栈和队列应用


第三章 栈、队列和数组

小题考频:23
大题考频:4


3.3 栈和队列的应用

难度:☆☆☆

知识总览

3.3.1_栈在括号匹配中的应用

3.3.2_1_栈在表达式求值中的应用(上)

在这里插入图片描述

3.3.2_2_栈在表达式求值中的应用(下)

在这里插入图片描述

3.3.3_栈在递归中的应用

3.3.4+3.3.5_队列的应用


3.3.1_栈在括号匹配中的应用

在这里插入图片描述

  • 括号都是成双成对出现的;
    左括号和右括号在数量上必须匹配,形状上也必须匹配。

在这里插入图片描述

  • 最后出现的左括号最先被匹配(LIFO
    ——后进先出

将这些左括号依次压入栈中,最后入栈的左括号最先被弹出(匹配)

在这里插入图片描述

每当出现一个右括号,就“消耗”一个左括号

  • 当我们遇到左括号时,就把它压入栈中,当我们遇到右括号时,就把栈顶的那个左括号给弹出,检查是否匹配。

Eg1.

在这里插入图片描述

所有的左右括号都能匹配,匹配成功

Eg2.

在这里插入图片描述

扫描的右括号与栈顶的左括号不匹配,匹配失败

Eg3.

在这里插入图片描述

扫描到右括号但是栈已经空了——右括号单身,匹配失败

请添加图片描述

处理完所有括号但是栈非空——右括号单身,匹配失败


算法实现

处理流程:

在这里插入图片描述
代码实现:
在这里插入图片描述

传入:
一个字符型数组 - 用于存放括号;数组长度
 
初始化一个栈
 
扫描:
如果是一个左括号,压入栈中;
如果是一个右括号,首先判断栈是否为空:

  • 栈空则失败;
    栈不空则栈顶元素出栈,并判断与所扫描元素是否匹配
     

检索完全部括号后,栈空说明匹配成功

  • 总结:
    用栈实现括号匹配
    依次扫描所有字符,遇到左括号入栈,遇到右括号则弹出栈顶元素检查是否匹配。
     
    匹配失败情况:
    ①左括号单身②右括号单身③左右括号不匹配

3.3.2_1_栈在表达式求值中的应用(上)

在这里插入图片描述


表达式

——大家熟悉的算数表达式

在这里插入图片描述

由三个部分组成:操作数、运算符、界限符

界限符是必不可少的,反映了计算的先后顺序

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-看为整体;
 
一个中缀表达式可能会转换成多种不一样的后缀、前缀表达式


中缀表达式转后缀表达式(手算)

中缀转后缀手算方法
①确定中缀表达式中各个运算符的运算顺序
②选择下一个运算符,按照「左操作数 -> 右操作数 -> 运算符」的方式组合成一个新的操作数
③如果还有运算符没被处理,就继续②
在这里插入图片描述

  • 但是中缀表达式中的运算顺序不唯一,后缀表达式也不唯一。

遵守左优先原则:
在这里插入图片描述

不遵守左优先原则:

在这里插入图片描述

  • 遵守“左优先”原则,不要FreeStyle,保证手算和机算结果相同
  • “左优先”原则:只要左边的运算符能先计算,就优先算左边的
    ——后缀表达式中运算符出现的顺序中缀表达式中的运算顺序是一样的

客观来看两种都正确,只是“机算”结果是前者
算法的确定性:同样的输入只能得到同样的输出

Eg.

  • “左优先”原则:可保证运算顺序唯一

在这里插入图片描述

可以让最左边的加法先生效。


后缀表达式的计算(手算)

在这里插入图片描述
后缀表达式的手算方法:
从左往右扫描,每遇到一个运算符,就让运算符前面最近的两个操作数执行对应运算,合体为一个操作数

注意:两个操作数的左右顺序

Eg1.
在这里插入图片描述
Eg2.
在这里插入图片描述

特点:最后出现的操作数先被运算
== LIFO(后进先出)==
!!!


后缀表达式的计算(机算)

用栈实现后缀表达式的计算:
①从左往右扫描下一个元素,直到处理完所有元素
②若扫描到操作数则压入栈,并回到①;否则执行③
③若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到①

Step1:
在这里插入图片描述
Step2:
在这里插入图片描述
Step3:
在这里插入图片描述
Step4:
在这里插入图片描述
Step5:
在这里插入图片描述
Step6:
在这里插入图片描述
Step7:
在这里插入图片描述
Step8:
在这里插入图片描述

注意:先出栈的是“右操作数”

在这里插入图片描述

后缀表达式适用于基于栈的编程语言(stack-oriented programming language),如:Forth、PostScript

  • 思考:后缀表达式怎么转中缀表达式?

中缀表达式转前缀表达式(手算)

中缀转前缀手算方法
①确定中缀表达式中各个运算符的运算顺序
②选择下一个运算符,按照「运算符 -> 左操作数 -> 右操作数」的方式组合成一个新的操作数
③如果还有运算符没被处理,就继续②
“右优先”原则:只要右边的运算符能先计算,就优先算右边

Eg. 没有遵循右优先
在这里插入图片描述
Eg. 遵循右优先原则
在这里插入图片描述

练习:

中缀转后缀:“左优先”
在这里插入图片描述
中缀转前缀:“右优先”
在这里插入图片描述


前缀表达式的计算(机算)

用栈实现前缀表达式的计算:
从右往左扫描下一个元素,直到处理完所有元素
②若扫描到操作数则压入栈,并回到①;否则执行③
③若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到①

在这里插入图片描述

注意:先出栈的是“左操作数”


3.3.2_2_栈在表达式求值中的应用(下)

在这里插入图片描述

在这里插入图片描述

中缀表达式转后缀表达式(机算)

初始化一个栈,用于保存暂时还不能确定运算顺序的运算符
从左到右处理各个元素,直到末尾。可能遇到三种情况:
①遇到操作数。直接加入后缀表达式。
②遇到界限符。遇到“(”直接入栈;遇到“)”则依次弹出栈内运算符并加入后缀表达式,直到弹出“(”为止。注意:“(”不加入后缀表达式。
③遇到运算符。依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到“(”或栈空则停止。之后再把当前运算符入栈。

* /优先级高于+ -

按上述方法处理完所有字符后,将栈中剩余运算符依次弹出,并加入后缀表达式。


例1:

Step1:
在这里插入图片描述

操作数A,加入到后缀表达式中

Step2:
在这里插入图片描述

运算符 + ,栈空,入栈

Step3:
在这里插入图片描述

操作数B,加入到后缀表达式中

Step4:
在这里插入图片描述

运算符 - ,栈非空,依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,即弹出 + 号,再入栈 - 号

扫描到 - 号时,可以确定,他和 + 号中间夹住的操作数B,既要进行加法运算,也要进行减法运算,由于他们的优先级是相等的,那么可以根据左优先原则,可以让操作数B,先执行左边的运算符 + 运算,即弹出 + 号,让加法先生效

Step5:
在这里插入图片描述

操作数C,加入后缀表达式

Step6:
在这里插入图片描述

运算符 * ,此时栈顶元素是 - 法,优先级低于当前运算符,同时也不能直接运算乘法(如果后面有括号就不能确定能否先运行),先压入栈中

Step7:
在这里插入图片描述

操作数D,加入后缀表达式

Step8:
在这里插入图片描述

运算符 / ,此时栈顶元素是 * ,优先级等于当前运算符,弹出 * 加入后缀表达式;此时栈顶元素时 - ,优先级低于当前运算符,但 / 号无法判断,入栈 / 号

Step9:
在这里插入图片描述

操作数E,加入后缀表达式

Step10:
在这里插入图片描述

运算符 + ,此时栈顶元素是 / 号,优先级高于当前运算符,弹出 / 加入后缀表达式;此时栈顶元素是 - 号,优先级与当前运算符相等,弹出 - 号加入后缀表达式;入栈 + 号

Step11:
在这里插入图片描述

操作数F,加入后缀表达式

Step12:
在这里插入图片描述

处理完所有字符,将栈中剩余运算符依次弹出


例2(带有界限符):

Step1:
在这里插入图片描述
Step2:
在这里插入图片描述
Step3:
在这里插入图片描述
Step4:
在这里插入图片描述
Step5:
在这里插入图片描述

界限符 - 左括号“ ( ”,直接入栈

遇到界限符。遇到“(”直接入栈;遇到“)”则依次弹出栈内运算符并加入后缀表达式,直到弹出“(”为止。注意:“(”不加入后缀表达式。

Step6:
在这里插入图片描述
Step7:
在这里插入图片描述

运算符 - ,此时需要依次弹出栈中优先级更高或相等的所有运算符,碰到“ ( ”停止,入栈 - 号

碰到“ ( ”,即左边的操作数旁边有一个“ ( ”,此时不可以确定 - 号是否立即生效,确定不了运算顺序,压入栈中

Step8:
在这里插入图片描述
Step9:
在这里插入图片描述

界限符 - 右括号“ ) ”,依次弹出栈中的运算符,直到弹出“ ( ”为止
——左括号“ ( ”不加入后缀表达式
优先生效括号内的部分

Step10:
在这里插入图片描述
Step11:
在这里插入图片描述
Step12:
在这里插入图片描述
Step13:
在这里插入图片描述
Step14:
在这里插入图片描述


例3(练习):

在这里插入图片描述


中缀表达式的计算(用栈实现)

中缀转后缀 + 后缀表达式求值
——两个算法的结合

用栈实现中缀表达式的计算:
初始化两个栈,操作数栈运算符栈
若扫描到操作数,压入操作数栈
若扫描到运算符或界限符,则按照“中缀转后缀”相同的逻辑压入运算符栈(期间也会弹出运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈顶元素并执行相应运算,运算结果再压回操作数栈

  • 上节后缀表达式的计算(机算)中,
    ——栈用于存放当前暂时还不能确定运算次序的操作数
  • 本节中缀表达式转后缀表达式(计算)中,
    ——栈用于用于存放当前暂时还不能确定运算次序的运算符

两个算法结合,设置两个栈,即可实现中缀表达式的计算

例:

Step1:

在这里插入图片描述

操作数A,入栈

Step2:
在这里插入图片描述

运算符 + ,栈空,入栈

Step3:
在这里插入图片描述

操作数B,入栈

Step4:
在这里插入图片描述

运算符 - ,
① 栈顶 + 优先级相等,弹出 + 号,
② 并弹出两个栈顶操作数,
③ 进行运算后,压入操作数栈
类似“中缀转后缀”

Step5:
在这里插入图片描述

操作数C ,入栈

Step6:
在这里插入图片描述

运算符 * ,栈顶 - 优先级低于其,入栈 * 号

Step7:
在这里插入图片描述

操作数D ,入栈

Step8:
在这里插入图片描述

运算符 / ,栈顶 * 优先级相等,弹出 * ,弹出两个操作数,运算后压如操作数栈;
栈顶 - 低于其,入栈 / 号

Step9:
在这里插入图片描述

操作数E ,入栈

Step10:
在这里插入图片描述

运算符 + ,栈顶 / 优先级高于其,弹出 / ,弹出两个操作数,运算后压入操作数栈;
栈顶 - 优先级相等,弹出 - ,弹出两个操作数,运算后压入操作数栈

Step11:
在这里插入图片描述

操作数F ,入栈

Step12:
在这里插入图片描述

扫描完所有,将运算符栈剩余运算符弹出,并弹出对应的操作数进行计算

  • WHY?
    Q:搞这么复杂的意义是什么?
    A:将复杂的问题拆分成一个个最简单的加减乘除运算,拆分成让CPU能够执行的最基本的指令。把算数表达式编译成与其等价的机器指令时,就需要用到这种中缀表达式计算的思想和方法。

3.3.3_栈在递归中的应用

函数调用背后的过程

在这里插入图片描述
函数调用的特点:最后被调用的函数最先执行结束(LIFO)- 栈

函数调用时,需要用一个栈存储:
①调用返回地址
②实参
③局部变量

系统会创建函数调用栈,用来保存各个函数在调用过程中必须保存的信息。
在这里插入图片描述
比如:

  1. 运行main函数时,会把main函数中必要的信息压入栈中:局部变量a,b,c;
  2. main函数会调用func1函数,把func1执行结束之后,应该继续执行的代码的存储地址压入栈中(#1),函数调用的两个参数也会压入栈中(a,b),局部变量(x)也压入栈中;
    ——所以函数中修改a,b的值,修改的只是内存中func1的a,b的值,不会影响到main的a,b值
  3. func1函数会调用func2函数,记录func2执行结束之后我们应该回到哪一句继续执行,把该句代码的存储地址(#2)压入栈中,调用的实参(x)和局部变量(m,n)也存入栈中;
  4. func2执行结束后,由栈顶的信息得知,应该往后执行#2句代码,然后把和func2相关的代码弹出栈,释放空间;
    同样的,func1执行结束后,由栈顶信息得知,应该往后执行#1句代码,将func1相关信息删除,接着#1句代码继续执行。

其实在main函数之前,还需要把某些不知道的信息压入栈底。

IDE中:
在这里插入图片描述


栈在递归中的应用

适合用“递归”算法解决:可以把原始问题转换为属性相同,但规模较小的问题

例1

计算正整数的阶乘n!
在这里插入图片描述

把n!转换成n*(n-1)!,问题规模变小了,且属性相同

在这里插入图片描述
递归调用时,函数调用栈可称为“递归工作栈”
每进入一层递归,就将递归调用所需信息压入栈顶
每退出一层递归,就从栈顶弹出相应信息

在这里插入图片描述

问题规模逐渐收敛,直到n = 1,开始逐层返回到第一层的调用n = 10,那么factorial(n - 1)的调用返回值应该是9!,最后一层,再乘上n = 10,即10!返回给main函数的192行代码,将factorial(10)的值赋给x

缺点:太多层递归可能会导致栈溢出,内存资源是有限的

IDE中:
在这里插入图片描述


例2

求斐波那契数列
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

传入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)

函数调用时,需要用一个“函数调用栈”存储:
①调用返回地址
②实参
③局部变量

递归调用时,函数调用栈可称为“递归工作栈”
每进入一层递归,就将递归调用所需信息压入栈顶
每退出一层递归,就从栈顶弹出相应信息

缺点:效率低,太多层递归可能会导致栈溢出;可能包含很多重复计算

可以自定义栈将递归算法改造成非递归算法


3.3.4+3.3.5_队列的应用

树的层次遍历

在这里插入图片描述

注:在“树”章节中会详细学习

树是分层的,层次遍历:即一层一层的遍历,就需要队列的辅助。

新建一个队列,从根节点出发按层次遍历各个结点

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:可用队列实现
——先申请的,先分配资源

  • Eg1:CPU资源的分配

在这里插入图片描述
Step1:多个进程排成就绪队列;
在这里插入图片描述
在这里插入图片描述

Step2:选择队头元素,上CPU执行一个短的时间片,迅速下CPU进入队尾;

在这里插入图片描述
Step3:重复Step2,选择队头元素,上CPU执行一个短的时间片,迅速下CPU进入队尾

这样所有进程都轮流得到了CPU处理。

  • Eg2:打印数据缓冲区

场景:去学校打印店打印论文,多个同学用同一台打印机打印,打印的先后顺序如何?
在这里插入图片描述
系统开辟了一片缓冲区用于存放打印机此时暂时不能处理的数据,排成队列依次打印。


知识回顾与重要考点

3.3.1_栈在括号匹配中的应用

用栈实现括号匹配
依次扫描所有字符,遇到左括号入栈,遇到右括号则弹出栈顶元素检查是否匹配。

匹配失败情况:
①左括号单身②右括号单身③左右括号不匹配


3.3.2_1_栈在表达式求值中的应用(上)

在这里插入图片描述

  • 一个中缀表达式可以对应多个后缀、前缀表达式;
    加入了“左优先”和“右优先”原则,使一个中缀表达式只对应一个后缀表达式、一个前缀表达式;
    ——确保算法的“确定性
  • 中缀转后缀,先弹出的元素是“右操作数”;
    中缀转前缀,先弹出的元素时“左操作数”
  • “左优先”、“右优先”、“左操作数”、“右操作数”是DIY的说法,方便理解

3.3.2_2_栈在表达式求值中的应用(下)

在这里插入图片描述

  • 用栈实现中缀转后缀
  • 用栈实现后缀表达式的计算
  • 用栈实现中缀表达式的计算(结合前两个算法)

3.3.3_栈在递归中的应用

  • 函数调用的特点:最后被调用的函数最先执行结束(LIFO)

  • 函数调用时,需要用一个“函数调用栈”存储:
    ①调用返回地址
    ②实参
    ③局部变量

  • 递归调用时,函数调用栈可称为“递归工作栈”

  • 每进入一层递归,就将递归调用所需信息压入栈顶

  • 每退出一层递归,就从栈顶弹出相应信息

缺点:效率低,太多层递归可能会导致栈溢出;可能包含很多重复计算

可以自定义栈将递归算法改造成非递归算法


3.3.4+3.3.5_队列的应用

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/668299
推荐阅读
相关标签
  

闽ICP备14008679号