如果我们有一些表达式中的括号这样式儿的:[{()}],那么我们可以发现从左往后开始,最先被匹配的括号是(),其次是{},最后才是[]。所以对于左括号而言,最后出现的左括号“(”被最先匹配,这就像栈中的最后进去的东西最先出来(LIFO),so可以用栈实现。
每出现一个右括号,就消耗一个左括号,这个“消耗”其实就是出栈操作。
那么我们想要用栈实现,我们该怎么实现?
首先我们知道这是个栈,然后最基础的流程就是遇到左括号压进去,遇到右括号就让栈顶元素出栈和这个右括号做匹配,匹配好了就继续往后扫描,没匹配到的话就说明有问题。
这个基础流程有很多需要注意的地方。第一是遇到右括号让栈顶元素出栈之前首先要判断是不是栈空(如果空就没有元素了弹个锤子),第二是就算弹出来的话也要判断是不是匹配的左右括号,第三是匹配完之后要看看有没有需要继续处理的括号了,如果已经没了就看看栈是不是空的,如果不是说明左括号多了。
其实一和三就是看看右括号是不是多了和看看左括号是不是多了。所以总结就两个情况:1.左/右是不是多了,2.括号是不是匹配。
故写代码第一个操作就是遇到的如果是左括号就先把左括号压栈,然后如果是右括号就先看看栈空不空,空就失败,不空弹出栈顶看看和它匹不匹配,一共就三种右括号,无论是哪一种右括号,如果弹出的不是和右括号对应的左括号,那就说明失败,其他情况就是成功。当然也不一定,如果全都遍历完了发现栈还不空,那也失败,三种失败情况对应以上三种需要注意的地方。
上代码:
#define MaxSize 10 //定义栈中元素的最大个数,如果担心存满可以用链栈。
typedef struct{
char data[MaxSize]; //静态数组存放栈中元素
int top; //栈顶指针
}SqStack;
bool bracketCheck(char str[],int length){
SqBracketStack S;
InitStack(S); //初始化一个栈
for(int i = 0; i<length; i++){
if(str[i] == '(' || str[i] == '[' || str[i] == '{'){
Push(S, str[i]); //扫描到左括号,入栈
}else{
if(StackEmpty(S)){ ///扫描到右括号,且当前栈空
return false; //匹配失败
}
char topElem;
Pop(S, topElem); //栈顶元素出栈
if(str[i] == ')' && topElem != '('){
return false;
}
if(str[i] == ']' && topElem != '['){
return false;
}
if(str[i] == '}' && topElem != '{'){
return false;
}
}
}
return StackEmpty(S); //检索完全部括号后,栈空说明匹配成功
}
可以伪的地方:
//初始化栈
void InitStack(SqStack &S)
//判断栈是否为空
bool StackEmpty(SqStack S)
//新元素入栈
bool Push(SqStack &S, char x)
//栈顶元素出栈,用x返回
bool Pop(SqStack &S,char &x)
某道说考研时候也可以直接使用伪。不伪也没事,这些基本操作都在上一篇讲过,也可以直接写。
小总结:匹配失败情况:1.左括号单身;2.右括号单身;3.左右括号不匹配。
一般一个计算表达式是有三个东西组成的:操作数,运算符,界限符。
比如1*(2+3),123就是操作数,*+就是运算符,()就是界限符。
像我们平常这种就是中缀表达式,你可以理解为运算符在中间(我就是这么理解的),所以如果运算符在后面就是后缀表达式,运算符在前面就是前缀表达式。
比如,1×(2+3),它的后缀表达式就是123+×,它的前缀表达式就是×1+23。再比如,a+b-c×d这个中缀表达式,它的后缀表达式就是ab+cd×-,前缀表达式就是 -+ab×cd。
后缀表达式叫逆波兰式(Reverse Polish notation),前缀表达式叫波兰式(Polish notation)。
(小声:为什么会有后缀表达式这种东西出现呢?是因为希望可以不用界限符也能无歧义地表达运算顺序。智慧真是无限啊~)
如果你想把一个中缀表达式(比如1*(2+3))转换为后缀表达式,你是怎么转换的?首先1拿出来然后肉眼看到2+3肯定先算2+3所以是23+,1拿出来是和2+3的结果乘所以是123+*。
所以我们中缀转后缀
的手算方法就是:(为啥说手算,因为后面会说机算)
- 确定中缀表达式中
各个运算符的运算顺序
;- 选择下一个运算符,按照左操作数 右操作数 运算符的方式组合成一个新的操作数;
- 如果还有运算符没被处理,就继续2。
例如,1*(2+3)肯定先计算的是+,+又是2和3+,所以是23+;23+当成一个操作数c,那么现在就变成了1*c,操作数是c,所以就是1c *,所以这个的后缀表达式就是123+ *.
按照“左优先”原则,即只要左边的运算符能先计算,就优先算左边的,这样是为了保证手算机算运>算唯一性。
现在我们已经得到了后缀表达式,那么我们该怎么计算呢?
比如你已经得到了一个123+*,那么你该怎么算出它的值?按照第一反应肯定是看到+就把2+3,然后得出的结果就是1乘以刚刚的2+3,所以我们的第一反应是先看运算符的。
所以后缀表达式的计算方法:
从左往右扫描,每遇到一个运算符,就让运算符前面最近的两个操作数执行对应运算,合成一个操作数.
我们刚刚的例子就是这么干的。但是要注意两个操作数的先后顺序,加和乘倒不注意没事,减和乘不注意就有事了,所以要注意。
之前不说这个用栈吗,那怎么用栈?我们按照手算的逻辑实现机算的话,首先就肯定还是等到看到运算符才进行第一步计算,所以之前那些东西肯定是压入栈的。so就能得出,我们得看到操作数先进栈,j进栈一直等待,等到看到运算符就等到了,弹出两个栈顶的操作数进行操作即可。
用栈实现
后缀表达式的计算:
从左往右
扫描下一个元素,直到处理完所有元素;- 若扫描到操作数则压入栈,并回到1,否则执行3;
- 若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到1。
又请出我们的老演员:123+*,那么这个栈现在从底往上的顺序就是123,现在扫描到操作符了,显然就要弹出2和3进行操作,那么2+3就是5,进行运算之后压回栈顶,所以想爱你在我们的栈中元素变成了15,此时我们又开始扫描,扫描到那个乘号了,所以我们就要弹出1和5进行乘法运算。运算结果压回栈顶,那么此时栈里面就是5,就是我们想要的结果。
若表达式合法,则最后栈中只会留下一个元素,就是最终结果。
注:先出栈的是“右操作数”
为什么要这么注意呢?因为就像上面说的,我现在举的例子是加和乘,如果是减法,别忘了先弹出那个是被减数,后弹出的那个是减数酱紫。
拓展:后缀表达式适用于基于“栈”的编程语言(stack-oriented programming language),比如:Forth,PostScript等。
我们中缀转前缀的手算方法是:
- 确定中缀表达式中各个运算符的运算顺序;
- 选择下一个运算符,按照**运算符 左操作数 右操作数 **的方式组合成一个新的操作数;
- 如果还有运算符没被处理,就继续2。
可以看到中缀转前缀和中缀转后缀手算流程几乎没有差别,只有一个区别就是组成新的操作数的时候把运算符放到了最左边。
再一次请出我们的老演员: 1*(2+3),1*(2+3)肯定先计算的是+,+又是2和3+,所以是+23;+23当成一个操作数c,那么现在就变成了1*c,操作数是c,所以就是 * 1c ,所以这个的后缀表达式就是 * 1+23。
用栈实现后缀表达式的计算:
- 从右往左扫描下一个元素,直到处理完所有元素;
- 若扫描到操作数则压入栈,并回到1,否则执行3;
- 若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到1。
这个和后缀表达式的计算几乎一毛一样,就一点不同就是第一个不是从左往右扫描了,是从右往左扫描。
注:先出栈的是“左操作数”
那么同理,计算我们应该遵循“右优先”原则,只要右边的运算符能先计算,就优先算右边的。(比如*1+23,是先算2+3)。
1*(2+3),程序怎么变为123+*?
我们在1.1刚刚说过,中缀转后缀的手算流程是这个样子滴:
- 确定中缀表达式中
各个运算符的运算顺序
;- 选择下一个运算符,按照左操作数 右操作数 运算符的方式组合成一个新的操作数;
- 如果还有运算符没被处理,就继续2。
那么我们现在开始机算,当然想都不用想我们肯定是要用到栈了:
初始化一个栈,用来保存暂时还不确定运算顺序的运算符
。
从左到右处理各个元素,直到末尾,可能遇到三种情况:
- 遇到
操作数
。直接加入后缀表达式;- 遇到
界限符
。遇到“(”直接入栈,遇到“)”则依次弹出栈内运算符并加入后缀表达式,直到弹出“(”为>止。注意:“(”不加入后缀表达式。- 遇到
运算符
。依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到“(”或栈空则停止。之后再把当前运算符入栈。- 接上述方法处理完所有字符后,将栈中剩余运算符依次弹出,并加入后缀表达式。
举个栗子:又双叒叕请出我们的老演员: 1*(2+3),1*(2+3)先扫描到1,加入后缀表达式扫到*我们发现栈里面没有比它优先级高的也没有和它优先级相等的,遂入栈。扫到“(”入栈,扫到2加入后缀表达式(此时后缀表达式是12),扫到“+”入栈,扫到3加入后缀表达式(后缀表达式是123),这个时候扫到了“)”,不一样了奥,扫到了“)”则依次弹出栈内运算符并加入后缀表达式,所以我们弹出栈里面的+(左括号省略,不加入后缀表达式的),遍历完成后,栈里面还剩一个 * ,弹出并添加到后缀表达式,所以我们的后缀表达式变成了123+ *。
当然我们肯定要保证一下栈不会溢出,这个栈不能放着放着放不下了哈~
如果想用栈实现算1*(2+3)的值,那我们可以先把它转化为后缀表达式123+ *(见3.1),再用后缀表达式123+ * 机算(见1.2.2)求值。
才刚说过,转为后缀表达式是有个运算符栈的(遇到操作数加入后缀表达式,遇到运算符加入运算符栈中,有优先级高的则依次弹出啥的)记得不,然后后缀表达式求值也是有个栈的(因为我们扫描后缀表达式比如123+ *,是先把操作数都放到操作数栈里,然后如果扫描到运算符则弹出俩操作数进行运算,完了再压回栈里,这样循环最后压回栈里的只有一个操作数就是最后结果),还记得不?
所以流程就是:
初始化两个栈,
操作数栈和运算符栈
若扫描到操作数,则压入操作数栈
若扫描到运算符或界限符,则按照“中缀转后缀”相同的逻辑压入运算符栈(期间也会弹出运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈顶元素并执行相应运算,运算结果再压回操作数栈)
那我再举个栗子哈,1*(2+3),1*(2+3)先扫描到1,是操作数,压入操作数栈;扫到*我们发现栈里面没有比它优先级高的也没有和它优先级相等的,遂入栈;扫到“(”入栈,扫到2,是操作数,压入操作数栈(此时操作数栈是1,2,运算符栈是 " *( “),扫到“+”入栈,扫到3,是操作数,压入操作数栈(此时操作数栈是1,2,3),这个时候扫到了“)”,不一样了奥,扫到了“)”则依次弹出栈内运算符,栈里头是” *( +",弹一个+出来,上面不说弹一个运算一个吗,所以开始执行2+3运算,是5然后压回操作数栈,遍历完成后,运算符栈里面还剩一个 * ,弹出,弹出运算符就要弹俩操作数进行运算,操作数栈里面是1,5,运算完1 * 5,结果为5压入操作数栈中,这就是最后结果。
总算说完了前中后缀,来说点简单的。
我们都知道,函数调用其实调啊调先执行的是最后被调的,所以这个也很像栈的特点(LIFO),后调用的先执行。所以函数调用其实也是用栈的,嘻嘻。
函数调用时,需要用一个函数调用栈存储以下内容:
所以函数真的是用栈的。
一个简单的阶乘递归代码:
#include
int factorial(int x){
if(x == 1){
return 1; //递归出口
}else{
return x*factorial(x-1); //递归体
}
}
int main() {
printf("5的阶乘值为:%d\n",cc);
return 0;
}
输出结果:
5的阶乘值为:120
可想而知,当你传入5,就要求factorial(4),所以把5压栈;当你传入4,就要求factorial(3),4压栈…直到你求factorial(2),算出来的是2*factorial(1),factorial(1)是1,此时一切都云开月明。没求到的值先压入栈,总会一个一个出来的。
递归调用时,函数调用栈可称为“递归工作栈”
每进入一层递归,就将递归调用所需信息压入栈顶
用递归算法解决,可以把原始问题转换为属性相同,但规模较小的问题。
但是!!!这个栈的大小,至少得是递归的层数吧,所以如果层数很多,就会导致栈溢出
,所以用的时候也要考虑下空间复杂度。
当然,任何递归算法都可以改造成非递归算法,无非是比较麻烦一点而已。
上代码
#include
int Fib(int x){
if(x == 0){
return 0;
}else if(x == 1){
return 1;
}else{
return Fib(x-1) + Fib(x-2);
}
}
int main(void){
int x = Fib(4);
printf("%d",x);
return 0;
}
输出结果:
3
这个也是非常典型的递归,也是压栈。递归出口是当x=0和x=1的时候。然后你要求Fib(4)就要求Fib(3)和Fib(2),你要求Fib(3)就要求Fib(2)和Fib(1)。。。。一直递归到最深处,直到递归出口就是它的尽头。而且你发现没有,它包含了很多重复的计算,比如Fib(2),就会算两次。
所以综合来说,递归的缺点就是效率低,太多层递归可能会导致栈溢出,可能包含很多重复计算。
树的层次遍历
一个树的层次遍历是怎么遍历的?就是一层一层从左往右遍历。比如一个树,它的根节点是0,第二层是1,2,第三层是3,4,5,6,那它的层次遍历就不用说了,就是0123456。我们怎么得到这个遍历序列呢?就要借助队列来实现。
简单来说,我记得就是先让根节点入队,出队时访问它的左右结点,再让它的左右结点入队。然后出队一个,访问它的左右结点…以此类推这样。这符合先进先出的特性,所以队列就可以。以后还会学的,这就不多说了~
图的广度优先遍历
图有广度优先遍历和深度优先遍历,深度优先就是一直往下访问,广度优先就跟树的层次遍历似的,一层一层往下捋,所以也是用队列,这个之后也会讲到。
队列在操作系统中的应用
在多个进程争抢使用有限的系统资源时,FCFS(First Come First Service,先来先服务)是一种常用策略,可用队列实现,比如CPU的分配,打印缓冲区数据什么的。
(缓冲区就是用队列组织打印数据,可以缓解主机和打印机进度不匹配的问题)
主要就是前缀转后缀表达式,还有就是后缀表达式的求值(如果是前缀表达式的求职,就先转成后缀表达式完了再后缀表达式求值),这个麻烦点,都用栈实现的。