LeetCode刷题栈使用总结学习笔记

2022-09-24

LeetCode刷题栈使用总结学习笔记。

栈基础知识

栈:先进后出

  1. 栈只能从表的一端存取数据,另一端封闭。

  2. 栈的具体实现:

(1)顺序栈:采用数组实现

(2)链栈:采用链式存储结构实现,链表实现

  1. 顺序栈及基本操作

顺序表模拟栈存储结构常用的实现思路:在顺序表中设定一个实时指向栈顶元素的变量(一般命名为 top),top 初始值为 $-1$,表示栈中没有存储任何数据元素,即栈是”空栈”,一旦有数据元素进栈,则 top 就做 $+1$ 操作,反之,如果数据元素出栈,top 就做 $-1$ 操作。

(1)顺序栈元素进栈,从栈顶进入,实现代码如下:

在想象一个栈的工作方式时,可以想象一下餐厅流水线开始时的一堆盘子。当餐厅的工作人员补充餐盘时,他或她放入的第一个盘子将是最后一个被取走的。

图1 如果某个算法需要**首先处理序列中最后保存的元素**,那么栈对于这种算法而言是非常有用的数据结构。 例如,计算机系统在执行程序时就会使用栈。当某个函数被调用时,计算机系统会将程序的返回地址、函数的参数以及函数的局部变量保存在栈中。当函数返回时,这些局部变量、参数和返回的地址等都将被从栈上删除。 ## 栈操作 栈有两个主要操作:入栈(push,也被称为压栈)和出栈(pop,也被称为弹栈)。 入栈操作会导致一个值被存储,或者被压入栈。例如,假设有一个空的整数栈,最多可以保存 3 个值。有了这个栈,则可以执行以下入栈操作: push(5); push(10); push(15); 图 2 显示了在执行这些入栈操作之后的栈状态。 ![](https://img-blog.csdnimg.cn/img_convert/021d42382a704feccd275b4fc1d55206.gif)
图2 出栈操作将从栈中检索(继而删除)一个值。假设要在如图 2 所示的栈上执行 3 个连续的出栈操作,结果将如图 3 所示。 ![](https://img-blog.csdnimg.cn/img_convert/e7895535ed326b0e26923b8edc68f0c0.gif)
图3 ```c++ /* 栈 */ #define MaxSize 10 // 栈中元素的最大个数 #include #include #include using namespace std; typedef int ElemType; struct SqStack { ElemType data[MaxSize]; // 静态数组存放栈顶元素 int top; // 栈顶指针 }; // 初始化栈 void InitStack(SqStack &S) { S.top = -1; } // 判断栈空 bool StackEmpty(SqStack S) { if (S.top == -1) { return true; } else { return false; } } // 入栈 bool Push(SqStack &S, ElemType x) { if (S.top == MaxSize - 1) { return false; } else { S.top++; S.data[S.top] = x; // S.top指向栈顶 return true; } } // 出栈 bool Pop(SqStack &S, ElemType &x) { if (S.top == -1) { // 栈空 return false; } else { x = S.data[S.top--]; // 先赋值再-- return true; } } // 读取栈顶元素 bool GetTop(SqStack S, ElemType &x) { if (S.top == -1) { return false; } else { x = S.data[S.top]; return true; } } bool DestroyStack(SqStack &S) { S.top = -1; } int main() { SqStack S; InitStack(S); // 初始化 cout << "栈是否为空:" << StackEmpty(S) << endl; // 判空 for (int i = 0; i < 5; i++) { Push(S, i); } ElemType top, pop; GetTop(S, top); cout << "栈顶元素:" << top << endl; Pop(S, pop); cout << "出栈元素:" << pop << endl; GetTop(S, top); cout << "新栈顶元素:" << top << endl; DestroyStack(S); // 销毁栈 cout << "栈是否为空:" << StackEmpty(S) << endl; // 判空 } ``` ## 栈的应用之括号匹配  除了逆序输出问题之外,括号匹配问题也是栈的一个典型应用,即检查表达式中的括号是否完全匹配,例如对于表达式a / ( (b + c) *d 和a / ( (b + c) *d ),前者的括号不匹配而后者的括号匹配。 >  在某个字符串(长度不超过100)中有左括号、右括号和大小写字母:规定(与常见的算数式子一样)任何一个左括号都从内到外与它右边且距离最近的右括号匹配。写一个程序,找到无法匹配的左括号和右括号,输出原来的字符串,并在下一行标出不能匹配的括号。不能匹配的左括号用"$“标注,不能匹配的右括号用”?"标注。 ### 算法思想 每个右括号必定要与之前未被匹配的离它最近的一个进行匹配。因此,可以从左至右的顺序遍历整个字符串: - 遍历过程中如果遇到左括号,就将其放入栈中以待后续右括号的匹配; - 遇到右括号,若此时栈非空,则栈顶左括号必定和当前右括号匹配;相反,若此时栈为空,则表示当前右括号不存在与之匹配的左括号,右括号匹配失败。 - 当字符串全部遍历完后,若栈非空,表明栈中的左括号不存在与之匹配的右括号,左括号匹配失败。 ```c++ #include<bits/stdc++.h> using namespace std; stack brackets; int main(){ string str; while(cin>>str){ string answer(str.size(),' '); //设为 输入长度个 空格 for(int i=0; i<str.size(); i++){ if(str[i] == '(' ){ brackets.push(i); //压入左括号下标 }else if(str[i] == ')' ){ //如果是右括号 if(!brackets.empty()){ brackets.pop(); //栈不空则弹出,即有左括号匹配 }else{ answer[i] = '?'; //右括号不匹配 } } } while(!brackets.empty()){ answer[brackets.top()] = '$'; //左括号不匹配 brackets.pop(); } cout<<str<<endl; cout<<answer<<endl; } return 0; } ``` ## 栈的应用之中缀表达式转化为后缀表达式 ### 中缀表达式基本概念 中缀表达式:操作符以中缀形式处于操作数的中间,例:$1 + 2$。 后缀表达式:将运算符写在操作数之后,例:$12+$。 一个表达式E的后缀形式可以如下定义: - 如果$E$是一个变量或常量,则$E$的后缀式是$E$本身。 - 如果$E$是$E_1 op E_2$形式的表达式,这里$op$是任何二元操作符,则E的后缀式为$E_1’$ $E_2’$ $op$,这里$E_1’$和$E_2’$分别为$E_1$和$E_2$的后缀式。 - 如果$E$是$(E_1)$形式的表达式,则$E_1$的后缀式就是$E$的后缀式。 那么为什么要将中缀表达式变为后缀表达式呢? 与后缀表达式相比,中缀表达式不容易被计算机解析,后缀表达式更易于计算机计算。 将一个中缀表达式转换为后缀表达式的一般算法: 中缀转后缀转换过程需要用到栈,具体过程如下: 从左到右扫描字符串 1)如果遇到操作数,我们就直接将其输出 。 2)如果遇到操作符,当栈为空直接进栈,不为空,判断栈顶元素操作符优先级是否比当前操作符小,小的话直接把当前操作符进栈,不小的话栈顶元素出栈输出,直到栈顶元素操作符优先级比当前操作符小 3)遇到左括号时我们也将其放入栈中。 4)如果遇到一个右括号,则将栈元素弹出,将弹出的操作符输出直到遇到左括号为止。注意,左括号也要弹出 ### 实例及详细过程 ```c++ #include "iostream" #include "queue" #include "map" #include "stack" #include "cstring" using namespace std; /** * 中缀表达式转后缀表达式 * @return */ int main(){ char bds[1000]; map<char,int> yxj; queue q; stack s; yxj['*'] = 2; yxj['/'] = 2; yxj['+'] = 1; yxj['-'] = 1; cin>>bds; int len = strlen(bds); for (int i = 0; i <len ; ++i) { if(bds[i]>='0'&&bds[i]<='9'){//操作数直接进队列中 q.push(bds[i]); }else if(bds[i] == '(' || s.size() == 0){//左括号直接入栈 s.push(bds[i]); }else if(bds[i] == ')') {//右括号需要处理到上一个(位置 while(s.top()!='('){ q.push(s.top());s.pop(); } s.pop();//弹出左括号 }else{//操作符 while(s.size() && yxj[s.top()]>=yxj[bds[i]]){ q.push(s.top());s.pop(); } s.push(bds[i]); } } while (s.size()){//栈中可能还有操作符,需要输出 q.push(s.top());s.pop(); } while(q.size()){ cout<<q.front();q.pop(); } return 0; } ``` ## 栈的应用之表达式求值(不带括号和带括号的表达式求值) 中缀表达式转换为后缀表达式的手工做法为: - 按照运算符的优先级对所有的运算单位加括号。 例二: ((a/b) + (((c*d) - (e*f))/g)) - 把运算符号移动到对应括号的后面,然后去掉括号。例二: ((ab)/ (((cd)*(ef)*)-g)/+,去掉括号ab/cd*ef*-g/+ > 以(a+b)*c-d为例: > 第一步先对运算符两边加括号: > (((a+b)*c)-d) > 第二步针对每个括号,我们将运算符移动到括号后面: > (((ab+)c*)d)- > 第三部去掉括号 > ab+c*d- 设置一个栈,开始时,栈为空,然后从左到右扫描后缀表达式,若遇操作数,则进栈; 若遇运算符,则从栈中退出两个元素,先退出的放到运算符的右边,后退出的 放到运算符左边,运算后的结果再进栈,直到后缀表达式扫描完毕。 此时,栈中仅有一个元素,即为运算的结果。 ### 实现 例如: 我们求3.5.2.-*7.+@的值 约定’@’为表达式的结束符号 ‘.’为操作数的结束符号。 核心代码如下: ```c++ stack s; char c; int sum = 0; while(cin>>c && c!='@') {//扫描表达式字符串 if (c >= '0' && c <= '9') {//数字 sum = sum * 10 + c - '0'; } else if (c == '.') {//操作数结束,入栈 s.push(sum); sum = 0; } else {//操作符 int y = s.top(); s.pop(); int x = s.top(); s.pop(); s.push(cult(x, y, c)); } } cout<<s.top(); ```