第三讲 栈、队列和数组 (1)

文章目录

    • 第三讲 栈、队列和数组
      • 3.1 栈
        • 3.1.1 出栈元素的不同排列与卡特兰数
        • 3.1.2 栈的顺序表实现
        • 3.1.3共享栈
        • 3.1.4 栈的链表实现
        • 3.1.5 栈的两种实现的优缺点
        • 3.1.6 c++中的栈( s t a c k stack stack)容器适配器
        • 3.1.7 栈的应用:star:
          • 3.1.7.1 **栈在括号匹配中的应用**
          • 3.1.7.2 **栈在表达式求值中的应用:star:**:star:
          • 3.1.7.3 **栈的应用—–递归**
          • 3.1.7.4 栈的其他典型应用
        • 3.1.8 栈的相关算法题
        • 3.1.8 栈的相关算法题

第三讲 栈、队列和数组

考频统计】:

年份考点分值
2009队列的应用、栈和队列的出入操作单选 * 2 = 4分
2010栈的出入操作、受限双端队列的出入操作单选 * 2 = 4分
2011栈的出入操作、循环队列的判空单选 * 2 = 4分
2012栈的应用:中缀转后缀单选 2分
2013栈的出入操作单选 2分
2014栈的应用:中缀转后缀、循环队列的判空判满单选 * 2 = 4分
2015栈的应用:递归单选 2分
2016队列的出入操作、三对角矩阵的压缩存储单选 * 2 = 4分
2017栈的综合考察、稀疏矩阵的压缩存储单选 * 2 = 4分
2018栈在表达式求值中的应用、栈与队列的出入操作、对称矩阵的压缩存储单选 * 3 = 6分
2019队列的设计(判空、判满等)应用题 10分
2020三角矩阵的压缩存储、栈的出入操作单选 * 2 = 4分
2021受限双端队列的出入操作、二维数组的存储单选 * 2 = 4分
2022栈的出入序列单选 2分
2023稀疏矩阵的存储之三元组单选 2分

考情分析】:本章每年都会出2个选择题左右,重点掌握栈与队列的出入操作、循环队列的判空判满、栈在表达式求值中的应用、矩阵压缩存储的下标计算等知识点;

考点预测及重点指出】:

  1. 掌握用顺序表和链表实现的队列和栈的判空判满条件
  2. 对于数组实现的栈注意区分top==0top == -1入栈时是先进栈再自增还是先自增再进栈。
  3. 栈在括号匹配中的作用;
  4. 初始化两个栈,操作数栈运算符栈,实现中缀表达式直接计算(包含了中缀转后缀以及用栈实现后缀表达式的计算)
  5. 循环队列的判空判满的三种实现方法:牺牲一个空位、设置计数器count、设置bool型变量flag(回忆具体实现方法,记不清就去看);
  6. 谨记入队时front指针不会变,变化的是rear指针;出队时,rear指针不会变,变化的是front指针
  7. 记得看一下预测的应用题;

3.1 栈

3.1.1 出栈元素的不同排列与卡特兰数

栈对线性表的插入和删除是在位置上进行了限制,但是并没有对进出时机进行限制。也就说,刚进去的元素也可以立即出栈,也可以等待一会儿再在合适的时机出栈,只要保证当前位置是栈顶即可

n n n个不同元素进栈,出栈元素不同排列的个数 N N N可由**卡特兰( C a t a l a n Catalan Catalan)**数确定: N = 1 n + 1 C 2 n n = ( 2 n ) ! n ! ( n + 1 ) ! N=\frac{1}{n+1}C^n_{2n}=\frac{(2n)!}{n!(n+1)!} N=n+11C2nn=n!(n+1)!(2n)!

比如5个元素进栈的话出栈不同排列的个数为 1 5 + 1 C 10 5 = 1 6 ( 10 ∗ 9 ∗ 8 ∗ 7 ∗ 6 5 ∗ 4 ∗ 3 ∗ 2 ∗ 1 ) = 42 \frac{1}{5+1}C^5_{10}=\frac{1}{6}(\frac{10*9*8*7*6}{5*4*3*2*1}) = 42 5+11C105=61(54321109876)=42

栈混洗( s t a c k p e r m u t a t i o n stack\ permutation stack permutation):将栈 A A A 的栈顶元素弹出并压入栈 S S S ,或将栈 S S S 的栈顶元素弹出并压入栈$ B$ ,经过一系列的操作后, $A $中的元素全部转入 B B B 中,则称之为 A A A 的一个栈混洗。由于栈 A A A 和栈 S S S 的弹出与压入次序不一样,由此产生了在栈 B B B 中的不同排列方式。若栈A中元素个数为n,其栈混洗种类个数为卡特兰数。具体原理见邓俊辉的栈那一节。

img

卡特兰数还有另外一个重要用途,即计算二叉树的形态数

image-20230808145011393

先序序列(前序序列)为 a, b, c, d 的不同二叉树的个数是?

【解】先序序列为入栈次序中序序列为出栈序列,因为前序序列和中序序列可以唯一确定一棵二叉树,所以相当于“以序列 a, b, c, d为入栈次序,则出栈序列的个数是?”

$N =\frac{1}{5}C^4_8=\frac{70}{5}=14 $

进栈出栈操作与二叉树中序遍历的关系(这也就是二叉树的中序遍历非递归(迭代)的操作方式):

  • 一个结点进栈后有两种处理方式:要么立即出栈(此时该入栈结点没有左孩子),要么下一个结点进栈(有左孩子);
  • 一个结点出栈后也有两种处理方式:要么继续出栈(该结点无右孩子),要么下一个结点进栈(有右孩子)
class Solution {
public:vector<int> inorderTraversal(TreeNode* root) {vector<int> result;stack<TreeNode*> st;TreeNode* cur = root;while (cur != NULL || !st.empty()) {if (cur != NULL) { // 指针来访问节点,访问到最底层st.push(cur); // 将访问的节点放进栈cur = cur->left;                // 左} else {cur = st.top(); // 从栈里弹出的数据,就是要处理的数据(放进result数组里的数据)st.pop();result.push_back(cur->val);     // 中cur = cur->right;               // 右}}return result;}
};

3.1.2 栈的顺序表实现

注意题目中top指针指向的是栈顶元素,还是说指向栈顶元素的下一个位置,条件不同,相应的基本操作也会发生变化,下面给出的源码top指针指向的就是下一个位置。

#define DataType int   //用DataType这个宏定义来统一代表栈中数据的类型,这里将它定义为整型,根据需要可以定义成其它类型,例如浮点型、字符型、结构体 等等;
#define bool int     
#define maxn 100010     //maxn代表我们定义的栈的最大元素个数;struct Stack {                 //定义栈的结构体DataType data[maxn];       //DataType data[maxn]作为栈元素的存储方式,数据类型为DataType,可以自行定制;int top;        // top即栈顶指针,data[top-1]表示栈顶元素,top == 0代表空栈;
};void StackClear(struct Stack* stk) {     //初始化,清空栈,在这里我们规定top指向栈顶元素的下一个存储单元,当然也可以将top设置为指向当前栈顶元素的存储单元,那样的话需要修改为stk -> top = -1;stk->top = 0;
}
bool PushStack(struct Stack *stk, DataType dt) {     //入栈操作,stk是一个指向栈对象的指针,由于这个接口会修改栈对象的成员变量,所以这里必须传指针,否则,就会导致函数执行完毕,传参对象没有任何改变;if(stk-> top == maxn){return false;         //栈满,报错}else{stk->data[ stk->top++ ] = dt;       // 将传参的元素放入栈中, 然后将栈顶指针自增 1return true;}
}bool PopStack(struct Stack* stk) {      //这里的出栈操作仅仅将栈顶元素弹出,如果需要用这个元素做什么的话弹出时可以用一个临时变量来接收它;if(stk->top == 0){return false;    //栈空,报错;}else{--stk->top;          //出栈 操作,只需要简单改变将 栈顶 减一 即可return true;}
}//下面为只读接口DataType StackGetTop(struct Stack* stk) {     //获取栈顶元素:数组中栈元素从 0 开始计数,所以实际获取元素时,下标为 栈顶元素下标 减一;return stk->data[ stk->top - 1 ];
}
int StackGetSize(struct Stack* stk) {          //获取栈大小,因为只有在入栈的时候,栈顶指针才会加一,所以它 正好代表了 栈元素个数;return stk->top;
}
bool StackIsEmpty(struct Stack* stk) {return !StackGetSize(stk);      //当 栈元素 个数为 零 时,栈为空
}
3.1.3共享栈

利用栈底位置相对不变的特性,可以让两个顺序栈共享一个一维数组空间,将两个栈的栈底分别设置在共享空间的两端,两个栈顶向共享空间的中间延伸,如图所示,如果栈顶指针指向当前元素的话,仅当两个栈顶指针相邻时,判断为栈满。image-20230728142722681

共享栈是为了更有效地利用存储空间,两个栈的空间相互调节,只有在整个存储空间被占满时才发生上溢。其存取数据的时间复杂度均为 O ( 1 ) O(1) O(1),所以对存取效率没有什么影响。

栈的上溢就是缓冲区满还往里写,栈的下溢就是缓冲区空还往外读,为了解决上溢,可以给栈分配很大的空间,而这样又会造成空间的浪费,共享栈的提出就是在解决上溢的基础上节省存储空间,将两个栈放在同一段更大的空间内。


3.1.4 栈的链表实现

不需要掌握,只要知道链栈的操作都是在表头进行的就够了;

采用链式存储的栈称为链栈,其优点是便于多个栈共享存储空间和提高其效率,且不存在栈满上溢的情况。为了方便操作,通常采用不带头结点的单链表实现,并规定所有操作都是在单链表的表头进行的。

尾结点就相当于栈底。

image-20230728144823515
  1. **定义链栈,并实现基本操作(要求单链表实现,栈顶在链头)**王道版本,带有头结点
//有头节点版本,栈为空时,栈中仅有一个头结点。
//定义栈结点
typedef struct SNode{			//定义单链表结点类型int data;				        //每个节点存放一个数据元素struct SNode *next;			//指针指向下一个节点
}SNode, *LiStack;//初始化一个链栈(单链表实现,栈顶在链头)
bool InitStack(LiStack &S) {S = (SNode *) malloc(sizeof(SNode)); //分配一个头结点S->next = NULL;         //头结点之后暂时还没有节点return true;
}//判断栈是否为空
bool StackEmpty(LiStack S){if(S->next==NULL)   //头结点后面没有结点return true;    //返回true,表示栈为空elsereturn false;
}//入栈(本质上是单链表的“头插法”)
bool Push (LiStack &S, int x){SNode * p = (SNode *) malloc(sizeof(SNode));    //新分配一个结点p->data = x;     //存入新元素p->next = S->next;S->next = p;     //新结点插入到头结点后面return true;
}//出栈(本质上是单链表的“头删法”)
bool Pop (LiStack &S, int &x){if (StackEmpty(S))      //栈空,出栈操作失败return false;SNode * p = S->next;     //栈不空,链头结点出栈x = p->data;             //x返回栈顶元素S->next = p->next;       //头删法,栈顶元素"断链"free(p); return true;
}
  1. 用c语言实现链栈的结构体定义与基本操作,不带头节点
//没有头结点的版本,栈为空时,top==NULL;
typedef struct StackNode {                // 定义链栈结点;int data;      //每个结点存放一个数据元素;struct StackNode *next;   //指针指向下一个节点;
}StackNode;struct Stack {     //定义链栈;               StackNode *top;        // top作为 栈顶指针,当栈为空的时候,top == NULL;否则,永远指向栈顶;int size;                    // 由于 求链表长度 的算法时间复杂度是 O(n) 的, 所以我们需要记录一个size来代表现在栈中有多少元素。每次入栈时size自增,出栈时size自减。这样在询问栈的大小的时候,就可以通过 O(1)的时间复杂度。
};//入栈操作,其实就是类似 头插法,往链表头部插入一个新的结点;
void PushStack(struct Stack *stk, int dt) {StackNode *insertNode = (StackNode *) malloc(sizeof(StackNode)); // 利用malloc生成一个链表结点insertNode;insertNode->next = stk->top;     // 将 当前栈顶 作为insertNode的 直接后继结点;insertNode->data = dt;           // 将 insertNode的 数据域 设置为传参 dt;stk->top = insertNode;           // 将insertNode作为 新的栈顶;++ stk->size;                    //栈元素 加一;
}//出栈操作,由于链表头结点就是栈顶,其实就是删除这个链表的头结点的过程。
void PopStack(struct Stack* stk) {if(stk->top == NULL) {//返回错误;}StackNode *temp = stk->top;  // 将 栈顶指针 保存到temp中;stk->top = temp->next;              //将 栈顶指针 的 后继结点 作为新的 栈顶;free(temp);                         // 释放之前 栈顶指针 对应的内存;--stk->size;                        // 栈元素减一;   
}//清空栈,即栈的初始化,可以理解为,不断的出栈,直到栈元素个数为零。
void StackClear(struct Stack* stk) {while(!StackIsEmpty(stk)) {       //判断当前栈是否为空PopStack(stk);           // 每次操作其实就是一个 出栈 的过程,如果 栈 不为空;则进行 出栈 操作,直到 栈 为空;}stk->top = NULL;                  //  然后将 栈顶指针 置为空,代表这是一个空栈了;stk->size = 0;
}//只读接口
int StackGetTop(struct Stack* stk) {return stk->top->data;                 // stk->top作为 栈顶指针,它的 数据域 data就是 栈顶元素的值,返回即可;
}
int StackGetSize(struct Stack* stk) {return stk->size;                      //  size记录的是 栈元素个数;
}int StackIsEmpty(struct Stack* stk) {return !StackGetSize(stk);      //当 栈元素 个数为 零 时,栈为空。
}
  1. 定义链栈,并实现基本操作(要求双链表实现,栈顶在链尾)
//定义栈结点
typedef struct DbSNode{			    //定义双链表结点类型int data;				        //每个节点存放一个数据元素struct DbSNode *last,*next;	    //指向前后两个结点
}DbSNode;typedef struct DbLiStack{	        //双链表实现的栈(栈顶在链尾)struct DbSNode *head, *rear;    //两个指针,分别指向链头、链尾
}DbLiStack, *DbStack;//初始化一个链栈(单链表实现,栈顶在链头)
bool InitDbStack(DbStack &S) {S = (DbLiStack *) malloc(sizeof(DbLiStack));    //初始化一个链栈(双链表实现,栈顶在链尾)DbSNode * p = (DbSNode *) malloc(sizeof(DbSNode)); //新建一个头结点p->next = NULL;         //头结点之后暂时还没有节点p->last = NULL;         //头结点之前没有节点S->head = p;S->rear = p;return true;
}//判断栈是否为空
bool DbStackEmpty(DbStack S){if(S->head == S->rear)   //头指针和尾指针指向同一个结点return true;    //返回true,表示栈为空elsereturn false;
}//入栈(在双链表链尾插入)
bool DbPush (DbStack &S, int x){DbSNode * p = (DbSNode *) malloc(sizeof(DbSNode));     //新分配一个结点p->data = x;     //存入新元素p->next = NULL;p->last = S->rear;  //新结点插入链尾S->rear->next = p;S->rear = p;return true;
}//出栈(删除双链表链尾元素)
bool DbPop (DbStack &S, int &x){if (DbStackEmpty(S))     //栈空,出栈操作失败return false;DbSNode * p = S->rear;    //栈不空,链尾结点出栈x = p->data;             //x返回栈顶元素S->rear = p->last;       //更新链尾指针S->rear->next = NULL;free(p);                 //释放结点return true;
}

3.1.5 栈的两种实现的优缺点
  1. 顺序表实现:在利用顺序表实现栈时,入栈出栈的常数时间复杂度低,且清空栈操作相比链表实现可以做到 O ( 1 ) O(1) O(1),唯一的不足之处是:需要预先申请好空间,而且当空间不足时,需要进行扩容;
  2. 链表实现:在利用链表实现栈时,入栈出栈 的常数时间复杂度略高,主要是每插入一个栈元素都需要申请空间,(不过,如果入栈元素本身就是节点对象,那么可以省去初始化步骤,从而提高效率);每删除一个栈元素都需要释放空间,且 清空栈 操作是 O ( n ) O(n) O(n)的,直接将 栈顶指针 置空会导致内存泄漏。好处就是:不需要预先分配空间,且在内存允许范围内,可以一直 入栈,没有顺序表的限制。

3.1.6 c++中的栈( s t a c k stack stack)容器适配器

如果在算法题中想要用栈这个数据结构解决问题的话,自己实现的话有点不切实际,由于408考试允许用c++,因此我们可以用stack这个stl容器来做题;

常用的基本操作如下:

stack<int> stk;    //定义一个栈,数据类型可以看情况定义,可以定义为int,链表结点,二叉树结点等等,根据情况自行选择;stk.push(1);   //元素入栈;stk.pop();    //元素出栈,无返回值;int top = stk.top();     //访问栈顶元素;int size = stk.size();   //获取栈的长度;bool empty = stk.empty();    //判读栈是否为空

3.1.7 栈的应用⭐️

消除递归必须使用栈这个说法是错误的,迭代也可以用来消除递归,只需要简单的循环就可以完成。

表达式求值有很多方法,使用栈解决只是其中的一种。

3.1.7.1 栈在括号匹配中的应用
  1. 20. 有效的括号 需要掌握
  • 题目描述

    给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串 s ,

    判断字符串是否有效。有效字符串需满足:左括号必须用相同类型的右括号闭合。
    左括号必须以正确的顺序闭合。
    每个右括号都有一个对应的相同类型的左括号。

    样例输入:s = "()[]{}"

    样例输出:true

  • 解题原理:我们遍历给定的字符串 s s s。当我们遇到一个左括号时,我们会期望在后续的遍历中,有一个相同类型的右括号将其闭合。由于后遇到的左括号要先闭合,因此我们可以将这个左括号放入栈顶。

    当我们遇到一个右括号时,我们需要将一个相同类型的左括号闭合。此时,我们可以取出栈顶的左括号并判断它们是否是相同类型的括号。如果不是相同的类型,或者栈中并没有左括号,那么字符串 s s s无效,返回False

    在遍历结束后,如果栈空,说明我们将字符串 s s s中的所有左括号闭合,返回True,否则返回False

    注意到有效字符串的长度一定为偶数,因此如果字符串的长度为奇数,我们可以直接返回False,省去后续的遍历判断过程。

    我们可以发现,左右括号和入栈出栈( p u s h / p o p push/pop push/pop)其实是对应的关系,当栈为空时再想弹出就会失败,其实这和括号序列中多了一个右括号没有与之相匹配的左括号是一样的效果。

  • 代码:

class Solution {
public:bool isMatch(char a,char b){if((a == '(' && b == ')')|| (a == '[' && b == ']') || (a == '{' && b == '}')){return true;}return false;}bool isValid(string s) {stack<char> stk;      //定义一个栈,用来存放左括号if(s.size() % 2 == 1)  return false;     //如果字符串长度为奇数则不可能有效,直接返回false;for(int i = 0; i < s.size(); i++){     if(s[i] == '(' || s[i] == '[' || s[i] == '{')  stk.push(s[i]);     //遇到左括号入栈;else if(stk.empty()) return false;       //如果遇到了右括号但是栈已经空了,说明不是有效的括号,直接返回false;else if(! isMatch(stk.top(),s[i])) return false;     //如果遇到右括号且栈不为空,但是栈顶不是相匹配的左括号,返回false;else stk.pop();      //当前遍历到的右括号与栈顶左括号相匹配,弹出栈顶元素继续遍历;}return stk.empty();     //遍历结束后,栈为空则为有效,否则无效;}
};

  1. 进阶版,32. 最长有效括号 不用掌握
  • 题目描述:

    给你一个只包含 '('')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。

    示例1:

    输入:s = "(()"
    输出:2
    解释:最长有效括号子串是 "()"
    

    示例2:

    输入:s = ")()())"
    输出:4
    解释:最长有效括号子串是 "()()"
    
  • 解题思路:

    • 分析合法的括号序列,一定是以下两种中的一种:
    • ( 1 ) ( ⋯ ) (1)(\cdots) (1)()
      ( 2 )
    • ( 2 ) ( ⋯ ) ⋯ ( ⋯ ) (2)(\cdots)\cdots(\cdots) (2)()()
    • 我们可以选准备好一个栈,并且塞入一个元素-1。
    • 然后,遍历这个字符串,遇到左括号,无脑入栈;遇到右括号,无脑出栈;
    • 这时候,如果栈为空,表明右括号多出来一个,说明断层了,这时候把当前的位置下标入栈;否则,当前位置减去栈顶的位置就是一个合法的长度,计算后更新最大值。
    • 最后得到的最大值就是我们要求的字符串最大长度。
  • 解题代码

    class Solution {
    public:int longestValidParentheses(string s) {int maxans = 0;stack<int> stk;stk.push(-1);for(int i = 0; i < s.size(); i++){if(s[i] == '('){stk.push(i);    // 如果是左括号直接入栈即可}else{// 如果是右括号,存在两种情况// 1.如果前面可以有左括号和它进行匹配,那么就存在一个由左括号、右括号组成的子串// 2.如果前面没有左括号和它进行匹配,那么这个右括号就形成了新的边界。新的子串匹配时,起点必须在该边界右边stk.pop();if(stk.empty()){   //如果这时候栈为空,表示断层了,将当前右括号位置入栈;stk.push(i);}else{maxans = max(maxans,i - stk.top());    //否则,取栈顶元素和当前位置相减,必然是一个合法序列,更新最大长度;}}}return maxans;}
    };
    

3.1.7.2 栈在表达式求值中的应用⭐️⭐️
  1. 前缀、中缀、后缀表达式的互相转换
  • 前缀表达式:也称波兰式,指运算符处于两个操作数的前面
  • 中缀表达式:指运算符在两个操作数之间的位置
  • 后缀表达式:也称逆波兰式,指运算符处于两操作数后面

【例 1】已知中缀表达式:a+b-c*d

  • 先确定运算顺序:(a+b)-(c*d)

  • 前缀表达式:-+ab *cd

  • 后缀表达式:ab+ cd*-

【例 2】已知中缀表达式:a/b+(c*d-e*f)/g

  • 先确定运算顺序:(a/b)+(((c*d)-(e*f))/g)
  • 前缀表达式:+ /ab / -*cd *ef g
  • 后缀表达式:ab/ cd* ef*- g/ +
  1. 后缀表达式求值问题

我们转换表达式的目的就在于让计算机实现表达式的求值,并且去掉括号后表达式无歧义,或者说,计算机是不认识也不清楚中缀式的含义的,只有人能读懂,但是我们转换为前缀或者后缀后却可以借助栈的性质来完成计算

力扣有该算法的对应习题150. 逆波兰表达式求值

  • 题目:

    给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式。

    请你计算该表达式。返回一个表示表达式值的整数。

    注意:

    有效的算符为 ‘+’、‘-’、‘*’ 和 ‘/’ 。
    每个操作数(运算对象)都可以是一个整数或者另一个表达式。
    两个整数之间的除法总是 向零截断 。
    表达式中不含除零运算。
    输入是一个根据逆波兰表示法表示的算术表达式。
    答案及所有中间计算结果可以用 32 位 整数表示。

  • 示例:

    1. 输入:tokens = ["2","1","+","3","*"]
      输出:9
      解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
      
    2. 输入:tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"]
      输出:22
      解释:该算式转化为常见的中缀算术表达式为:((10 * (6 / ((9 + 3) * -11))) + 17) + 5
      = ((10 * (6 / (12 * -11))) + 17) + 5
      = ((10 * (6 / -132)) + 17) + 5
      = ((10 * 0) + 17) + 5
      = (0 + 17) + 5
      = 17 + 5
      = 22
      
  • 解题步骤:逆波兰表达式严格遵循「从左到右」的运算。计算逆波兰表达式的值时,使用一个栈存储操作数,从左到右遍历逆波兰表达式,进行如下操作:

    • 如果遇到操作数,则将操作数入栈;
    • 如果遇到运算符,则将两个操作数出栈,其中先出栈的是右操作数,后出栈的是左操作数,使用运算符对两个操作数进行运算,将运算得到的新操作数入栈。
    • 整个逆波兰表达式遍历完毕之后,栈内只有一个元素,该元素即为逆波兰表达式的值。
  • 参考代码:

    class Solution {
    public:int evalRPN(vector<string>& tokens) {stack<int> st;for (string s : tokens) {if (s == "+") {int a = st.top(); st.pop();int b = st.top(); st.pop();st.push(b+a);}else if (s == "-") {int a = st.top(); st.pop();int b = st.top(); st.pop();st.push(b-a);}else if (s == "*") {int a = st.top(); st.pop();int b = st.top(); st.pop();st.push(b*a);}else if (s == "/") {int a = st.top(); st.pop();int b = st.top(); st.pop();st.push(b/a);}else {st.push(stoi(s));    //将字符串转化为int型数字放入栈中;}}return st.top();    //栈内只有一个元素,该元素即为逆波兰表达式的值。}
    };
    

    如果想要优雅一点的话,可以这样写

    class Solution {
    public:int evalRPN(vector<string>& tokens) {stack<long long> s;for(auto x : tokens){if(x == "+" || x == "-" || x == "*" || x == "/"){long long nums1 = s.top();     //先弹出的为右操作数s.pop();long long nums2 = s.top();     //后弹出的为左操作数s.pop();if(x == "+") s.push(nums2 + nums1);else if(x == "-") s.push(nums2-nums1);else if(x =="*") s.push(nums2 * nums1);else s.push(nums2/nums1);}else s.push(stoi(x));       //遇到数字则入栈//stoi和atoi都是将字符串转成int类型,但是stoi的参数是const string*类型,//而atoi的参数是const char*类型;}long long ans = s.top();return ans;}
    };  
    
  1. 中缀表达式转后缀表达式的算法(选择题重要考点⭐️)

初始化一个栈,用来保存暂时还不能确定运算顺序的运算符。从左到右处理各个元素,直到末尾。可能遇到三种情况:

  • 扫描到操作数:直接加入后缀表达式;

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

  • 扫描到运算符

    1. 如果当前运算符优先级大于栈顶运算符,当前运算符直接入栈
    2. 如果当前运算符优先级小于等于栈顶运算符,需要弹出当前运算符优先级小于等于栈顶的运算符,直到栈空或遇到栈顶为左括号,最后当前运算符入栈。

    1+2+3*4*5举例,看是如何利用上述两个关键点实施计算的。

    首先,这个例子只有+和*两个运算符,所以它的运算符表是:00.webp.jpg

    这里的含义是:

    (1)如果栈顶是+,即将入栈的是+,栈顶优先级高,需要先计算,再入栈;

    (2)如果栈顶是+,即将入栈的是*,栈顶优先级低,直接入栈;

    (3)如果栈顶是*,即将入栈的是+,栈顶优先级高,需要先计算,再入栈;

    (4)如果栈顶是* ,即将入栈的是*,栈顶优先级高,需要先计算,再入栈;

算法实现:

#include <iostream>
#include <stack>
#include <string>
using namespace std;int getpriority(char c)//优先级判断
{if (c == '+' || c == '-'){return -1;//加减优先级小}else{return 1;//乘除优先级大}
}string convert(string& express)
{int i = 0;string ans;     //后缀表达式stack<char> s;    //初始化一个栈用于存放还没有确定运算顺序的运算符;while (express[i] != '\0')//扫描中缀表达式{if ('0' <= express[i] && express[i] >= '9')//如果扫描到了操作数,直接加入后缀表达式{ans += express[i];}else if (express[i] == '(')//如果扫描到了左括号,直接入栈{s.push(express[i++]);}else if (express[i] == '+' || express[i] == '-' || express[i] == '*' || express[i] == '/')//扫描到运算符进行优先级判断{if (s.empty() || s.top() == '(' || getpriority(express[i]) > getpriority(s.top()))//如果此时栈为空或者栈顶元素为左括号,或者扫描到的运算符优先级大于栈顶运算符优先级,则将当前运算符入栈;{s.push(express[i++]);}else//反之优先级如果是小于等于的话,那么就要把运算符出栈然后加入后缀表达式;{char temp = s.top();s.pop();ans += temp;}}else if (express[i] == ')')//最后一种情况就是扫描到了右括号,那么就把从栈顶到左括号的元素依次出栈加入后缀表达式;{while (s.top() != '('){char temp = s.top();s.pop();ans += temp;}//注意最后停止循环的时候栈顶元素是左括号,但是不要把左括号入栈,所以直接丢掉左括号s.pop();i++;//不要忘记后移}}while (!(s.empty()))//如果栈没有空,那么依次出栈,加入后缀表达式;{char temp = s.top();s.pop();ans += temp;}return ans;  
}
  1. 结合上面的后缀表达式计算和中缀转后缀的算法实现,我们可以用栈实现中缀表达式的计算;

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

    • 若扫描到操作数,则直接压入操作数栈;

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

    代码部分,要么重新按照这个逻辑写一下,要么直接将中缀表达式传入,然后分别调用上面两个函数就可以得到运算结果了;

    acwing 3302. 表达式求值,优秀解题报告:https://www.acwing.com/solution/content/40978/

    #include <iostream>
    #include <stack>
    #include <string>
    #include <unordered_map>
    using namespace std;stack<int> num;
    stack<char> op;//优先级表
    unordered_map<char, int> h{ {'+', 1}, {'-', 1}, {'*',2}, {'/', 2} };void eval()//求值
    {int a = num.top();//第二个操作数num.pop();int b = num.top();//第一个操作数num.pop();char p = op.top();//运算符op.pop();int r = 0;//结果 //计算结果if (p == '+') r = b + a;if (p == '-') r = b - a;if (p == '*') r = b * a;if (p == '/') r = b / a;num.push(r);//结果入栈
    }int main()
    {string s;//读入表达式cin >> s;for (int i = 0; i < s.size(); i++){if (isdigit(s[i]))//数字入栈{int x = 0, j = i;//计算数字while (j < s.size() && isdigit(s[j])){x = x * 10 + s[j] - '0';j++;}num.push(x);//数字入栈i = j - 1;}//左括号无优先级,直接入栈else if (s[i] == '(')//左括号入栈{op.push(s[i]);}//括号特殊,遇到左括号直接入栈,遇到右括号计算括号里面的else if (s[i] == ')')//右括号{while(op.top() != '(')//一直计算到左括号eval();op.pop();//左括号出栈}else{while (op.size() && h[op.top()] >= h[s[i]])//待入栈运算符优先级低,则先计算eval();op.push(s[i]);//操作符入栈}}while (op.size()) eval();//剩余的进行计算cout << num.top() << endl;//输出结果return 0;
    }
    

3.1.7.3 栈的应用—–递归

函数调用的特点:最后调用的函数最先被执行( L I F O LIFO LIFO)

函数调用时,需要用一个“函数调用栈”存储:调用返回值、实参、局部变量

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

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

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

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

后续在二叉树的遍历一节,我们可以看到如何用栈来讲递归算法改成迭代算法!


3.1.7.4 栈的其他典型应用
  • 浏览器中的后退与前进、软件中的撤销与反撤销。每当我们打开新的网页,浏览器就会将上一个网页执行入栈,这样我们就可以通过「后退」操作回到上一页面。后退操作实际上是在执行出栈。如果要同时支持后退和前进,那么需要两个栈来配合实现。
  • 程序内存管理。每次调用函数时,系统都会在栈顶添加一个栈帧,用于记录函数的上下文信息。在递归函数中,向下递推阶段会不断执行入栈操作,而向上回溯阶段则会执行出栈操作。

3.1.8 栈的相关算法题
  1. 20. 有效的括号
  2. 32. 最长有效括号
  3. 150. 逆波兰表达式求值
  4. 二叉树的各种遍历的非递归版本
  5. 225. 用队列实现栈
  6. 232. 用栈实现队列
  7. 206. 反转链表
  8. LeetCode 234. 回文链表
  9. 扩展:有时间看一下夜深人静写算法(十一)- 单调栈

acwing 3302. 表达式求值,


栈顶添加一个栈帧,用于记录函数的上下文信息。在递归函数中,向下递推阶段会不断执行入栈操作,而向上回溯阶段则会执行出栈操作。


3.1.8 栈的相关算法题
  1. 20. 有效的括号
  2. 32. 最长有效括号
  3. 150. 逆波兰表达式求值
  4. 二叉树的各种遍历的非递归版本
  5. 225. 用队列实现栈
  6. 232. 用栈实现队列
  7. 206. 反转链表
  8. LeetCode 234. 回文链表
  9. 扩展:有时间看一下夜深人静写算法(十一)- 单调栈

acwing 3302. 表达式求值,


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/840732.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

m1系列芯片aarch64架构使用docker-compose安装rocketmq5.0以及运维控制台

之前看到 DockerHub 上有大佬制作了 m1 芯片, aarch64架构的 rocketmq 镜像, 所以就尝试的安装了下, 亲测可用: 一. docker-compose.yml 文件命令 volumes 挂载目录需要换成自己的目录 注意 depends_on 标签, broker 和 console 的 启动要晚于 namesrv, 因为 broker 需要注册…

MPLS LDP原理与配置

1.LDP基本概念 &#xff08;1&#xff09;LDP协议概述 &#xff08;2&#xff09;LDP会话、LDP邻接体、LDP对等体 &#xff08;3&#xff09;LSR ID 与LDP ID &#xff08;4&#xff09;LDP消息 ⦁ 按照消息的功能&#xff0c;LDP消息一共可以分为四大类型&#xff1a;发现…

JKI State Machine的特点与详细介绍

JKI State Machine是一种基于状态机的LabVIEW架构&#xff0c;由JKI公司开发。它广泛用于开发复杂的应用程序&#xff0c;提供了一种灵活且可扩展的结构&#xff0c;适用于多种任务的管理和执行。其设计目标是提高开发效率、代码可读性和可维护性。 2. 基本架构 JKI State Ma…

【算法题】520 钻石争霸赛 2024 全解析

都是自己写的代码&#xff0c;发现自己的问题是做题速度还是不够快 520-1 爱之恒久远 在 520 这个特殊的日子里&#xff0c;请你直接在屏幕上输出&#xff1a;Forever and always。 输入格式&#xff1a; 本题没有输入。 输出格式&#xff1a; 在一行中输出 Forever and always…

python给图片加上图片水印

python给图片加上图片水印 作用效果代码 作用 给图片加上图片水印图片水印的透明度&#xff0c;位置可自定义 效果 原始图片&#xff1a; 水印图片&#xff1a; 添加水印后的图片&#xff1a; 代码 from PIL import Image, ImageDraw, ImageFontdef add_watermark(in…

体检系统商业源码,C/S架构的医院体检系统源码,大型健康体检中心管理系统源码

体检系统商业源码&#xff0c;C/S架构的医院体检系统源码&#xff0c;大型健康体检中心管理系统源码 体检信息管理系统软件是对医院体检中心进行系统化和规范化的管理。系统从检前&#xff0c;检中&#xff0c;检后整个业务流程提供标准化以及精细化的解决方案。实现体检业务市…

优化css样式的网站

一、按钮的css样式 https://neumorphism.io/#e0e0e0https://neumorphism.io/#e0e0e0 二、渐变样式 Fresh Background Gradients | WebGradients.com &#x1f48e;Come to WebGradients.com for 180 beautiful linear gradients in CSS3, Photoshop and Sketch. This collect…

Git Core Lecture

1、Git 简介 官方介绍&#xff1a;Git is a fast distributed revision control system (Git 是一个快速的分布式版本控制系统) 2、Git Core Command 2.1 git init git 工程初始化&#xff0c;会在工作区 (working directory) 根目录中创建.git 目录 # 创建目录 $ mkdir git-i…

C# 深拷贝和浅拷贝

文章目录 1.深拷贝2.浅拷贝3.拷贝类4.浅拷贝的实现5.深拷贝实现5.1 浅拷贝对象&#xff0c;对引用类型重新一个个赋值5.2 反射实现5.3 利用XML序列化和反序列化实现 1.深拷贝 拷贝一个对象时&#xff0c;不仅仅把对象的引用进行复制&#xff0c;还把该对象引用的值也一起拷贝。…

python期末作业:批量爬取站长之家的网站排行榜数据并保存,数据分析可视化

爬虫作业,含python爬取数据和保存文件,数据分析使用pyecharts做数据可视化 整体上分析网站的排名,直观看各个网站的热度。 数据分析之后大致的效果: 整个项目分为两个大的部分,第一部分就是抓取网站排名数据,然后保存为Excel、csv等格式,其次就是从文件中…

【30天精通Prometheus:一站式监控实战指南】第8天:redis_exporter从入门到实战:安装、配置详解与生产环境搭建指南,超详细

亲爱的读者们&#x1f44b;   欢迎加入【30天精通Prometheus】专栏&#xff01;&#x1f4da; 在这里&#xff0c;我们将探索Prometheus的强大功能&#xff0c;并将其应用于实际监控中。这个专栏都将为你提供宝贵的实战经验。&#x1f680;   Prometheus是云原生和DevOps的…

arc-eager算法XJTU-NLP自然语言处理技术期末考知识点

arc-eager算法&#xff1a;以我/做了/一个/梦为例来描述arc-eager算法的四个操作&#xff1a;shift&#xff0c;left-arc&#xff0c;right-arc&#xff0c;reduce XJTU-NLP期末考点2024版 题型&#xff1a;5*6简答题4*15计算题 简答题考点&#xff1a; &#xff08;1&#…

Java+Spring+ IDEA+MySQL云HIS系统源码 云HIS适合哪些地区的医院?

JavaSpring IDEAMySQL云HIS系统源码云HIS适合哪些地区的医院&#xff1f; 云HIS适合哪些地区的医院&#xff1f; 云HIS&#xff08;云医院信息系统&#xff09;适合多种地区的医院&#xff0c;特别是那些希望实现医疗服务的标准化、信息化和规范化&#xff0c;同时降低IT运营成…

42-2 应急响应之计划任务排查

一、进程排查 进程排查是指通过分析系统中正在运行的进程,以识别和处理恶意程序或异常行为。在Windows和Linux系统中,进程是操作系统的基本单位,因此对于发现和处理恶意软件或异常活动至关重要。恶意程序通常会以进程的形式在系统中运行,执行各种恶意操作,比如窃取信息、破…

每日一题 包含不超过两种字符的最长子串

目录 1.前言 2.题目解析 3.算法原理 4.代码实现 1.前言 首先我打算介绍一下&#xff0c;我对滑动窗口的理解。 滑动窗口可以分为四个步骤&#xff1a; 进窗口&#xff1a; 在这一步骤中&#xff0c;我们决定了要在窗口中维护的信息。例如&#xff0c;在这个问题中&#xff…

Codeforces Round 946 (Div.3)

C o d e f o r c e s R o u n d 946 ( D i v . 3 ) \Huge{Codeforces~Round~946~(Div.3)} Codeforces Round 946 (Div.3) 题目链接&#xff1a;Codeforces Round 946 (Div. 3) 文章目录 Problems A. Phone Desktop题意思路标程 Problems B. Symmetric Encoding题意思路标程 Pr…

ubuntu 配置用户登录失败尝试次数限制

前言&#xff1a; 通过修改pam配置来达到限制密码尝试次数&#xff01; 1&#xff1a;修改 /etc/pam.d/login 配置&#xff08;这里只是终端登录配置&#xff0c;如果还需要配置SSH远程登录限制&#xff0c;只配置下面的 /etc/pam.d/pam.d/common-auth 即可&#xff09; vim…

SpringCloud的Config配置中心,为什么要分Server服务端和Client客户端?

SpringCloud的Config配置中心&#xff0c;为什么要分Server服务端和Client客户端&#xff1f; 在SpringCloud的Config配置中心中分了Server服务端和Client客户端&#xff0c;为什么需要这样分呢&#xff1f;它的思想是所有微服务的配置文件都放到git远程服务器上&#xff0c;让…

Elasticsearch集群搭建学习

Elasticsearch集群聚合、集群搭建 RestClient查询所有高亮算分控制 数据聚合DSL实现Bucket聚合DSL实现Metrics聚合RestAPI实现聚合 拼音分词器如何使用拼音分词器&#xff1f;如何自定义分词器&#xff1f;拼音分词器注意事项&#xff1f; 自动补全数据同步集群搭建ES集群结构创…

蓝桥杯备赛——DP【python】

一、小明的背包1 试题链接&#xff1a;https://www.lanqiao.cn/problems/1174/learning/ 问题描述 输入实例 5 20 1 6 2 5 3 8 5 15 3 3 输出示例 37 问题分析 这里我们要创建一个DP表&#xff0c;DP&#xff08;i&#xff0c;j&#xff09;表示处理到第i个物品时消耗j体…