数据结构(四)----栈和队列的应用

目录

一.栈的应用

1.括号匹配问题

2.表达式求值问题

(1)三种算术表达式

(2)中缀表达式转后缀表达式

(3)中缀表达式转前缀表达式

(4)计算机实现中缀表达式转后缀表达式

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

3.递归

二.队列的应用

1.树的层次遍历

2.图的广度优先遍历

3.队列在操作系统中的应用


一.栈的应用

1.括号匹配问题

在代码中出现的括号必须是成对的,这就是括号匹配的问题。如下图所示,越往后出现的左括号会越先被匹配。这样的规律符合栈的后进先出(LIFO)原则。

如下图所示,没出现一个右括号,就"消耗"一个左括号。

下一个仍然是右括号,那么继续找"离该右括号最近的,没有被匹配的括号"

这样,当遇到一个"左括号"时,就进行压栈,当遇到一个"右括号"时,就将栈顶的“左括号”弹出,看两个括号是否匹配(例如”{“和")"就不匹配),就可以用栈进行括号匹配了。

出现下面情况,则说明括号匹配不成功:

1.当前扫描到的右括号与栈顶左括号不匹配

2.扫描到右括号且栈空,即右括号没有配对的左括号:

3.处理完所有括号后,栈非空,即左括号没有配对的右括号:

算法的流程如下:

代码如下:

#define MaxSize 10

typedef struct SqStack {
    char data[MaxSize];
    int top;
} SqStack;

//初始化栈
void Initstack(SqStack &s) {
    s.top = -1;
}

//判断栈是否为空
bool StackEmpty(SqStack s) {
    return s.top == -1;
}

//新元素入栈
bool Push(SqStack &s, char x) {
    if(s.top == MaxSize - 1)
        return false;
    s.data[++s.top] = x;
    return true;
}

//栈顶元素出栈,用x返回
bool Pop(SqStack &s, char &x) {
    if(s.top == -1)
        return false;
    x = s.data[s.top--];
    return true;
}

bool braketCheck(char str[], int length) {
    SqStack 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);    //检索完全部括号后,栈空说明匹配成功
}

2.表达式求值问题
(1)三种算术表达式

•中缀表达式

运算符在两个操作数的中间

•后缀表达式(逆波兰表达式,Reverse Polish notation)

运算符在两个操作数的后面

当然,对于“a+b-c”也可以先计算b-c,那么得出的后缀表达式为:a bc- +,所以一个中缀表达式可能对应多个后缀表达式

•前缀表达式(波兰表达式,Polish notation)

运算符在两个操作数前面

同理,对于“a+b-c”也可以先计算b-c,那么得出的前缀表达式为"+ -bc a",所以一个中缀表达式也可能对应多个前缀表达式

这部分常考表达式的相互转换问题,需要自己多动手练习,可以看看这个up主的视频:数据结构---前缀 中缀 后缀 表达式之间的转换_哔哩哔哩_bilibili

(2)中缀表达式转后缀表达式

中缀表达式:

后缀表达式:

中缀表达式:

后缀表达式:

运算顺序不唯一,对应的后缀表达式也不唯一:

中缀表达式:

后缀表达式:

客观来看两种都正确,只是“机算”结果是前者。算法得到的结果具有确定性,一个确定的输入只能得到一个确定的输出。

可以使用“左优先”原则来保证手算和机算的结果相同。就是只要左边的运算符能先计算,就优先算左边的。

例如上面的例子:

可以先让(C-D)中的"-"先生效,也可以让"E/F"中的"/"先生效。但是根据“左优先”原则,应该先让(C-D)中的"-"先生效。

再例如:

可以先让A+B中的“+”先生效,而不是C*D中的“*”。

得到的唯一后缀表达式为:

可以观察到,后缀表达式中的运算符是从左往右依次生效的。

后缀表达式的计算方法:

从左往右扫描,每遇到一个运算符,就让运算符前面最近的两个操作数执行对应运算,合体为一个操作数。

要注意两个操作数的左右顺序,例如上图,15/(7-(1+1))不能颠倒

根据后缀表达式的计算方法“运算符前面最近的两个操作数执行对应运算”,就是最后出现的操作数先被运算,这样的运算规律符合栈的后进先出(LIFO)特性。所以后缀表达式可以用栈实现。实现方法如下:

从左往右扫描下一个元素,直到处理完所有元素

②若扫描到操作数则压入栈,并回到①;否则执行③

③若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到①

如图所示,扫描到操作数则压入栈中:

扫描到运算符则,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶:

注意:先出栈的是右操作数,后出栈的是左操作数。

以此类推:

遇到“-”号,将两个栈顶元素取出,执行减法运算,再压回栈中。

最后得到中缀表达式:若表达式合法,则最后栈中只会留下一个元素,就是最终结果。

(3)中缀表达式转前缀表达式

“右优先”原则:只要右边的运算符能先计算,就优先算右边的。

若不用"右优先"原则:

中缀表达式:

前缀表达式:

可以看到,前缀表达式中的生效顺序是没有规律的。

若使用"右优先"原则:

中缀表达式:

前缀表达式:

可以看到,前缀表达式中的运算符是从右到左依次生效的

前缀表达式的计算:

从右往左扫描下一个元素,直到处理完所有元素

②若扫描到操作数则压入栈,并回到①;否则执行③

③若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到①

 总结一下(重要):

1.中缀表达式转后缀表达式:“左优先”原则

后缀表达式中的运算符从左到右依次生效:

2.中缀表达式转前缀表达式:"右优先"原则

前缀表达式中的运算符从右到左依次生效:

3.后缀表达式的计算中,是从左往右扫描到下一个元素。前缀表达式的计算中,是从右往左扫描到下一个元素。

4.后缀表达式的计算中先出栈的是“右操作数”,前缀表达式的计算中先出栈的是“左操作数”。

(4)计算机实现中缀表达式转后缀表达式

处理方法如下:

初始化一个栈,用于保存暂时还不能确定运算顺序的运算符。

从左到右处理各个元素,直到末尾。可能遇到三种情况:
①遇到操作数。直接加入后缀表达式。

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

注意:“(”不加入后缀表达式。

③遇到运算符。依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到“(”或栈空则停止。之后再把当前运算符入栈。
按上述方法处理完所有字符后,将栈中剩余运算符依次弹出,并加入后缀表达式。

遇到操作数,直接加入后缀表达式。遇到运算符,由于栈空,直接将运算符入栈

再次遇到运算符(“-”)时,需要依次弹出栈中优先级高于或等于当前运算符的所有运算符。由于“+”号和"-"号优先级相同,所以把“+”号弹出,并且把“-”号放入栈中。

为什么要这样做呢?

在中缀表达式中,出现一个操作数的话,操作数的左右两边一定有运算符(除了第一个和最后一个操作数外),之前的栈中存放的是一个“+”号,“+”号后面紧接着扫描到了“-”号,这说明“-”号和“+”号中间一定扫描到了一个操作数,而这个操作数既要进行加法运算也要进行减法运算。由于这两种运算的优先级是相等的。

根据“左优先”原则,可以让操作数先执行左边运算符对应的运算,所以进行了“+”号弹出栈的操作。

随后扫描到的是操作数,直接加入后缀表达式

继续扫描到“*”号运算符,由于当前栈中的运算符是“-”号,比“*”号优先级低,所以不弹出栈。

“*”号也不能弹出栈,若“*”号后面是(D/E), 那么“*”的优先级也不是最高的。

所以“*”入栈,操作数D直接放到后缀表达式中。

接下来遇到“/”,由于“*”号和“/”运算优先级相等,可以先让“*”先生效。

以此类推,所有的字符处理完毕后,将栈中剩余运算符依次弹出。

得到A B + C D * E / - F +

带有界限符号的例子:

前面部分和上面分析的相同:

扫描到界限符“(”时,直接入栈,左括号后面的“C”直接输出,“C”后面是"-"号,需要依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,但是此时栈顶是"(",所以不需要弹出任何元素,直接把运算符入栈:

为什么这样做呢?

因为栈顶是左括号的话,说明"-"号左边操作数的旁边有一个左括号,虽然括号里面的运算时优先的,但是扫描到"-"号时,我们不确定他后面的操作数有没有跟一个"*或/"(优先级更高的运算符)

也就是我们不确定"-"号是否能立即生效,所以暂时还不能确定运算顺序的运算符先压到栈中。

当我们遇到右括号时,依次弹出栈内运算符并加入后缀表达式,直到弹出"("为止。但是"("不加入后缀表达式,因为后缀表达式是没有界限符的。

因为扫到"("时,已经可以确定整个括号的范围了,由于需要优先计算括号内的内容,所以可以把括号中的运算符全部弹出,并放到后缀表达式中。

“)”后面是一个“-”号,所以先把栈中优先级高于或等于“-”号的运算符弹出,也就是“*”和“+”号都弹出,再把“-”号放入栈中。

接下来的操作,与上个例子分析的相同。

最后将栈内剩余的运算符依次弹出,加入后缀表达式即可。

注意:以下情况情况会出现栈溢出

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

中缀表达式的计算即“中缀转后缀”+“后缀表达式求值”,两个算法的结合(两个算法前面都详细讲过)。

后缀表达式的计算中,栈存放的是当前暂时还不能确定运算次序的操作数;而中缀表达式转后缀表达式的算法中,栈是用来存放当前暂时还不能确定运算次序的运算符

由于中缀表达转后缀表达式的过程是从左往右扫描的,后缀表达式的计算也是从左往右进行扫描的,所以可以将两者的过程结合起来,即一边生成后缀表达式,一边计算后缀表达式前半部分的值。

计算过程如下:

初始化两个栈,操作数栈运算符栈

若扫描到操作数,压入操作数栈

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

具体过程演示:

操作数放到操作数栈中,由于运算符栈空,所以“+”号可以直接入栈。

扫描到“-”号时,按照“中缀转后缀”的逻辑,由于运算符栈顶的“+”号和当前运算符“-”号优先级相同,所以弹出“+”号,并弹出操作数栈中的两个操作数,先出栈的为右操作数

并且把当前运算符“-”号压入栈中。

按照规则继续操作,当遇到“/”号时,会把运算符栈的栈顶元素”*“弹出。

并弹出两个操作数,进行乘法运算,并压回操作数栈的栈顶。

遇到“E”,直接压入操作数栈。

遇到"+"号,需要将”/“和“-”号依次弹出。

弹出“/”号,并将两个操作数出栈进行运算,最后压回栈顶。

弹出"-"号,将栈顶的两个操作数弹出进行减法运算,最后压回栈顶。最后要将当前扫描到的运算符"+"号,放入运算符栈中。

 最后让“F”入栈,并将运算符栈的运算符"+"号弹出,让操作数栈的两个栈顶操作数进行加法运算。

:在计算机中,将复杂的式子翻译为与之等价的机器指令时,就需要用到中缀表达式计算的思想和方法。

总结:

这部分多做练习就可以,会越做越简单的。

3.递归

递归算法可以把原始问题转换为属性相同,但规模较小的问题。由下图,我们可以观察到,最后被调用的函数最先执行结束,与栈的后进先出特性相同(LIFO)。

在程序运行之前,系统会开辟一个函数调用栈,用来保存函数调用过程中必须保存的信息,递归调用时,函数调用栈可称为“递归工作栈”。如:

① 调用返回地址

② 实参

③ 局部变量

如下图所示,a,b表示func1实参,x表示func1中的局部变量,#1表示func1返回地址;a,b,c表示main()的局部变量,以此类推。

当func函数执行完后,会被弹出。

下面代码为递归算法求阶乘的程序:

#include<stdio.h> 
//计算正整数n! 
int factorial(int n){
	if(n==0 || n==1)
		return 1;
	else
		return n*factorial(n-1);
}

int main(){
	int a;
	printf("请输入要求的阶乘:");
	scanf("%d",&a);
	int b=factorial(a);
	printf("%d",b);
}

其算法的内部逻辑如下图所示,每进入一层递归,就将递归调用所需信息压入栈顶

每退出一层递归,就从栈顶弹出相应信息,当n=1时,return 1,并从栈顶弹出相应信息。第9层的n=2,所以return n*factorial(1),即2*1,以此类推,到第1层时,返回的则是10*9!(9的阶乘),即10的阶乘。

注意:若有太多的递归层,就有可能导致栈溢出,因为内存有限,系统开辟的函数调用栈也是有上限的。 所以这也可以解释,为什么递归次数越多时间复杂度越高。

再例如用递归算法实现斐波那契数列:

#include<stdio.h> 
int Fib(int n){
    if(n==0)
        return 0;
    else if(n==1)
        return 1;
    else
        return Fib(n-1)+Fib(n-2);    
}

int main(){
    int a;
    printf("请输入要求第几个斐波那契数:");
    scanf("%d",&a);
    int b=Fib(a-1);
    printf("%d",b);
}

如下图所示,如果递归调用到Fib(2),就会return Fib(1)+Fib(0),首先会调用Fib(1),返回1

返回Fib(2)时,还需接着调用Fib(0),调用Fib(0),返回0。

到此Fib(2)相关的调用都有返回值了,所以Fib(2)也可以接着网上一层返回。

具体调用过程如下:

从上面可以看出,Fib(2),Fib(1)和Fib(0)都有被调用多次,所以递归调用中可能包含很多重复的计算。

二.队列的应用

1.树的层次遍历

新建一个队列,从根结点出发,按层次遍历各个结点,具体操作如下:

1.扫描根结点(1号结点),并将其左右孩子放到队列的队尾,接着1号结点出队。

②--->③ 

2.检查队头结点,将其左右孩子放到队列的队尾。

② ---> ③ ---> ④ ---> ⑤ 

处理完2号结点,2号结点出队。

③ ---> ④ ---> ⑤ 

3.以此类推,若某结点没有左右孩子(如4号结点),那么不需要将任何元素插到队列中,直接将该结点出队。

4.直到队列为空,完成树的层次遍历。

2.图的广度优先遍历

假设从1号结点出发,进行图的广度优先遍历:

1.遍历1号结点时,检查与1号结点相邻的结点有没有被遍历过,2号和3号结点都没有被遍历过,所以把它们放到队尾。

① ---> ② ---> ③

处理完 ① 号结点后,①号结点出队。

② ---> ③

2. 与 ② 号结点相邻的只有4号结点没被处理过,所以4号结点入队,2号结点出队。

③ ---> ④ 

3.以此类推,若与某结点相邻的结点都被处理过了,那么该结点直接出队。

4.直到队列为空,就完成了图的广度优先遍历。

3.队列在操作系统中的应用

多个进程争抢着使用有限的系统资源时,FCFS(First Come First Service,先来先服务)是一种常用策略。就是将进程放入缓冲区中(缓冲区用"队列"组织),排队使用有限资源。

  • 14
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值