【LeetCode】栈和队列进阶题目

刷爆LeetCode系列

LeetCode栈和队列进阶题:

github地址

有梦想的电信狗

前言

本文用C++实现LeetCode栈和队列的进阶题

  1. 最小栈
  2. 栈的压入弹出序列
  3. 逆波兰表达式求值

题目一、最小栈

题目描述

题目链接https://leetcode.cn/problems/min-stack/description/


题目与思路分析

目标分析

  • 实现一个最小栈数据结构,支持常规的栈操作(push、pop、top)
  • 要求 :在常数时间内检索到栈中的最小元素
  • 约束条件
    • 所有操作的时间复杂度为 O(1)
    • 空间复杂度可以适当放宽到 O(n)

核心挑战

  • 常规栈只能快速访问栈顶元素,无法直接获取最小值
  • 当最小元素被弹出时,需要快速确定新的最小值
  • 需要处理重复最小值的情况

思路 :使用双栈结构来维护数据

  • 主栈 (_stack):正常存储所有元素,支持常规栈操作
  • 最小栈 (_minStack)专门用于跟踪当前最小值


数据结构

使用两个栈来维护数据:

  • _stack:主栈,存储所有元素
  • _minStack:最小栈,存储当前最小值

操作逻辑

  • 构造函数

    cpp 复制代码
      MinStack() 
      { }
      // 该构造函数不实现,或者实现为空,都是正确的
      // 我们不写初始化列表,所有的成员也会走初始化列表
      // 内置类型不做处理,自定义类型调用默认构造函数

  • push操作 - 压入元素

    • 最小栈为空时:直接将元素压入两个栈

      cpp 复制代码
        if(_minStack.empty())
        {
            _stack.push(val);
            _minStack.push(val);
        }
    • 最小栈不为空时 :主栈压入元素,如果新元素 <= 最小栈栈顶,则最小栈也压入

      cpp 复制代码
        else
        {
            _stack.push(val);
            if(val <= _minStack.top())  // 注意是小于等于,处理重复最小值
                _minStack.push(val);
        }

  • pop操作 - 弹出元素

    • 先保存主栈栈顶元素,然后主栈弹出

      cpp 复制代码
        int topVal = _stack.top();
        _stack.pop();
    • 如果弹出的元素等于最小栈栈顶,最小栈也要弹出

      cpp 复制代码
        if(topVal == _minStack.top())
            _minStack.pop();
  • top操作 - 获取栈顶元素

    cpp 复制代码
      return _stack.top();
  • getMin操作 - 获取当前最小值

    cpp 复制代码
      return _minStack.top();  

关键点说明

  • 最小栈维护策略 :只有当新元素小于等于当前最小值时才压入最小栈
  • 弹出匹配:只有弹出的元素等于最小栈栈顶时,最小栈才弹出

代码实现

  • 时间复杂度 :所有操作都是 O(1)
  • 空间复杂度 :最坏情况下 O(n),但通常比使用单个栈+额外变量的方案更优
cpp 复制代码
class MinStack 
{
private:
    stack<int> _stack;
    stack<int> _minStack;

public:
    MinStack() 
    { }
    
    void push(int val) 
    {
        if(_minStack.empty())
        {
            _stack.push(val);
            _minStack.push(val);
        }
        else
        {
            _stack.push(val);
            if(_stack.top() <= _minStack.top())
                _minStack.push(val);
        }
    }
    
    void pop() 
    {
        int topVal = _stack.top();
        _stack.pop();
        if(topVal == _minStack.top())
            _minStack.pop();
    }
    
    int top() 
    {
        return _stack.top();
    }
    
    int getMin() 
    {
        return _minStack.top();  
    }
};

算法代码优化

  • push时 :不论哪种情况 _stack 都要 push,因此可以优化 为:
    • 最小栈为空时
    • 不为空时,只在 val 小于等于 最小栈的栈顶元素时 push
cpp 复制代码
void push(int val) 
{
    // 不论哪种情况 _stack 都要 push
    // 最小栈为空时直接 push,不为空时,只在 val 小于等于 最小的元素时才 push

    _stack.push(val);

    if(_minStack.empty() || _stack.top() <= _minStack.top())
        _minStack.push(val);
}

题目二、栈的压入弹出序列

题目描述

题目链接https://www.nowcoder.com/practice/d77d11405cc7470d82554cb392585106?tpId=13&&tqId=11174&rp=1&ru=/activity/oj&qru=/ta/coding-interviews/question-ranking


题目与思路分析

目标分析

  1. 判断给定的两个序列是否满足要求 :栈的压入序列与弹出序列匹配
    • 弹出序列必须严格符合**栈的后进先出(LIFO)**原则
    • 弹出的数字必须在它之前已经被压入
    • 每次弹出的值必须是当前栈顶的元素
  2. 要求 :时间复杂度为 O(N),空间复杂度为 O(N)

思路 :**模拟**真实的栈操作过程(使用一个辅助栈)

  • 按顺序将压入序列 pushV 的元素依次入栈
  • 每入栈一个元素,就检查当前栈顶是否与弹出序列 popV[popi] 匹配
    • 如果不匹配,继续入栈
    • 如果匹配,则不断从栈顶弹出,直到不再匹配
  • 最终,所有元素压入完成后:
    • 如果栈被弹空,说明弹出序列合法
    • 如果栈中还有残留元素,说明弹出序列不可能由压栈序列得到

操作 :可以依次遍历压入序列 pushV 来进行模拟:

由于算法进行的过程是每入栈一个元素后就判断出栈序列是否能匹配 ,因此**当入栈序列被全部入栈后,就已经进行了判断出了栈的压入弹出序列是否匹配**。因此循环的结束条件如下:

cpp 复制代码
while (pushi < pushV.size())

  • 将当前压入序列的元素入栈

    cpp 复制代码
    st.push(pushV[pushi++]);

  • 入栈后检查是否可以进行弹出匹配

    • 如果栈顶元素和当前弹出目标 popV[popi] 不相等,说明当前不能弹出

      cpp 复制代码
      if(st.top() != popV[popi])
          continue;   // 不匹配,继续入栈

  • 如果栈顶与弹出序列当前值匹配,则可能会连续弹出多个元素:

    • 可能出现:
      *
      1. 连续匹配,栈被一直弹空
        1. 弹出几个后遇到不匹配的情况,然后继续入栈
    cpp 复制代码
    while(!st.empty() && st.top() == popV[popi])
    {
        st.pop();
        ++popi;
    }

所有元素压入完后,检查栈是否为空

  • 如果栈为空 → 所有弹出序列都成功匹配
  • 如果栈不为空 → 弹出序列一定是非法的
cpp 复制代码
return st.empty();
  • 最终返回 true or false

代码实现

cpp 复制代码
class Solution 
{
public:
    // 模拟出栈的过程
    bool IsPopOrder(vector<int>& pushV, vector<int>& popV) 
    {
        // 两个序列的数据个数一致才可能匹配
        if(pushV.size() != popV.size())
            return false;
        
        int pushi = 0, popi = 0;
        stack<int> st;

        // 把所有元素都入栈了,也就判断完了,循环条件为 pushi < pushV.size()
        while (pushi < pushV.size()) 
        {
            st.push(pushV[pushi++]);
            // 不匹配,继续入,重复判断逻辑,因此写成循环
            if (st.top() != popV[popi]) 
            {
                continue;
            }
            // 匹配,有可能一直匹配,可能会把栈出空
            // 出空了,就继续入   或者 出到不匹配了也继续入
            else 
            {
                // 出栈有两种情况
                // 1. 一直匹配,一直出栈,出到栈为空了,就继续 push 数据
                // 2. 出了几个匹配的,一次pop后遇到不匹配的了,就继续入栈
                while(!st.empty() && st.top() == popV[popi])
                {
                    st.pop();
                    ++popi;
                }
            }
        }
        // 所有元素都入栈后
        // 如果全都匹配,栈会被pop空
        // 只要栈不为空,说明不匹配
        return st.empty();
    }
};

算法代码优化

观察到弹出序列不匹配时,我们执行的是continue逻辑继续执行循环

cpp 复制代码
  if (st.top() != popV[popi]) 
  {
      continue;
  }

整个 while 循环中只有两个分支

  • 不相等时不匹配:继续循环,继续入数据
  • 相等时可能匹配连续出栈判断是否全部匹配

因此可以将 if else 逻辑优化掉,仅仅处理匹配时的情况

  • 不匹配时什么都不做,继续在最外层while循环中push数据
  • 如果能匹配且栈为非空时,就持续pop数据,判断是否是连续的匹配序列
cpp 复制代码
while(pushi < pushV.size())
{
    st.push(pushV[pushi++]);

    // 匹配,有可能一直匹配,可能会把栈出空
    // 出空了,就继续入   或者 出到不匹配了也继续入
    while(!st.empty() && st.top() == popV[popi])
    {
        st.pop();
        ++popi;
    }
}

完整代码如下

cpp 复制代码
class Solution
{
public:
    // 模拟出栈的过程
    bool IsPopOrder(vector<int>& pushV, vector<int>& popV)
    {
        // 两个序列的数据个数一致才可能匹配
        if(pushV.size() != popV.size())
            return false;
        
        int pushi = 0, popi = 0;
        stack<int> st;
        while(pushi < pushV.size())
        {
            st.push(pushV[pushi++]);

            // 匹配,有可能一直匹配,可能会把栈出空
            // 出空了,就继续入   或者 出到不匹配了也继续入
            while(!st.empty() && st.top() == popV[popi])
            {
                st.pop();
                ++popi;
            }
        }
        return st.empty();
    }
};

题目三、逆波兰表达式求值

前置知识:中缀表达式与后缀表达式

1. 中缀表达式

定义 :中缀表达式是我们日常生活中最熟悉、最常用的数学表达式书写方式。它的特点是运算符位于两个操作数中间

示例

  • A + B
  • (A + B) * C
  • (3 + 4) * (5 - 2)

特点

  • 直观易读:对人类非常友好,符合我们的思维习惯。
  • 需要处理优先级 :运算符有不同的优先级(例如,乘除 * / 高于加减 + -)。
  • 可能需要括号 :为了改变默认的运算顺序,必须使用括号 ( )

缺点

对于计算机来说,直接处理中缀表达式比较困难,因为它需要不断地判断运算符的优先级和括号,运算顺序不明确。


2. 后缀表达式

定义 :后缀表达式,也称为逆波兰表示法 。它的特点是运算符位于两个操作数之后

示例

  • 中缀 A + B 的后缀形式是 A B +
  • 中缀 (A + B) * C 的后缀形式是 A B + C *
  • 中缀 (3 + 4) * (5 - 2) 的后缀形式是 3 4 + 5 2 - *

特点

  • 无需括号:表达式的运算顺序完全由运算符的位置决定,不需要括号。
  • 无需优先级:所有运算符的优先级是相同的,计算时严格按照从左到右的顺序遇到运算符就执行。
  • 适合计算机计算:计算机可以使用一个栈(Stack)非常高效地对后缀表达式进行求值。

为什么适合计算机?

因为后缀表达式的求值过程是一个严格的、线性的过程,无需复杂的优先级判断和括号匹配。


对比总结

特性 中缀表达式 后缀表达式
运算符位置 操作数 之间 操作数 之后
可读性 ,对人类友好 ,对人类不直观
计算复杂性 ,需要处理优先级和括号 ,顺序固定,无需括号
求值方法 需要两个栈(操作数栈和运算符栈) 只需要一个栈(操作数栈)
括号 需要 不需要

核心:如何将中缀表达式转换为后缀表达式?

转换过程使用一个栈来暂存运算符,并遵循以下算法:

  1. 初始化一个空栈(用于存放运算符)和一个空列表(用于输出后缀表达式)。
  2. 从左到右扫描中缀表达式。
  3. 如果遇到操作数,直接将其加入输出列表。
  4. 如果遇到左括号 (,将其压入栈中。
  5. 如果遇到右括号 ),则依次弹出栈顶的运算符并加入输出列表,直到遇到左括号为止。然后将左括号弹出(但不加入输出列表)。
  6. 如果遇到运算符 (假设为 op1):
    • 比较 op1 与栈顶运算符 op2 的优先级:
      • 如果栈为空,或栈顶是左括号 (,或 op1 的优先级高于 op2,则将 op1 压入栈。
      • 否则,弹出栈顶的 op2 并加入输出列表,然后回到步骤6,再次比较 op1 和新的栈顶运算符。
  7. 当表达式扫描完毕后,将栈中所有剩余的运算符依次弹出并加入输出列表。

示例:将中缀 (3 + 4) * (5 - 2) 转换为后缀

步骤 扫描到的字符 操作 栈(底->顶) 输出列表
1 ( 压入栈 (
2 3 输出 ( 3
3 + 栈顶是(,压入+ ( + 3
4 4 输出 ( + 3 4
5 ) 弹出直到(,输出+ 3 4 +
6 * 栈空,压入* * 3 4 +
7 ( 压入栈 * ( 3 4 +
8 5 输出 * ( 3 4 + 5
9 - 栈顶是(,压入- * ( - 3 4 + 5
10 2 输出 * ( - 3 4 + 5 2
11 ) 弹出直到(,输出- * 3 4 + 5 2 -
12 结束 弹出栈中剩余的* 3 4 + 5 2 - *

最终的后缀表达式为:3 4 + 5 2 - *


核心:如何计算后缀表达式?

计算过程使用一个栈来存放操作数,算法非常简单:

  1. 初始化一个空栈。
  2. 从左到右扫描后缀表达式。
  3. 如果遇到操作数,将其压入栈。
  4. 如果遇到运算符 ,则从栈中弹出两个 操作数(注意顺序:先弹出的是右操作数,后弹出的是左操作数 ),进行运算,将运算结果压回栈中
  5. 重复步骤2-4,直到表达式扫描完毕。此时,栈中唯一的元素就是表达式的最终结果。

示例:计算后缀表达式 3 4 + 5 2 - *

步骤 扫描到的字符 操作 栈(底->顶)
1 3 压入栈 3
2 4 压入栈 3, 4
3 + 弹出 4 (右),弹出 3 (左),计算 3 + 4 = 7,压入 7 7
4 5 压入栈 7, 5
5 2 压入栈 7, 5, 2
6 - 弹出 2 (右),弹出 5 (左),计算 5 - 2 = 3,压入 3 7, 3
7 * 弹出 3 (右),弹出 7 (左),计算 7 * 3 = 21,压入 21 21
8 结束 计算完成,结果为栈顶元素 21

最终结果为:21 ,与中缀表达式 (3+4)*(5-2) 的计算结果一致。

总结

  • 中缀表达式:给人看的,直观但有优先级和括号问题。
  • 后缀表达式:给计算机算的,不直观但计算效率极高。

许多编译器和计算器内部都会先将中缀表达式转换为后缀表达式,然后再进行求值,以优化性能。


题目描述

题目链接https://leetcode.cn/problems/evaluate-reverse-polish-notation/description/



题目与思路分析


目标分析

  1. 判断给定的逆波兰表达式是否可以正确求值 :逆波兰表达式求值
    • 遇到数字,需要被压入栈
    • 遇到操作符,必须从栈中弹出两个操作数
    • 操作符一定作用于最近压入的两个数字(栈顶两个)
    • 计算后的结果继续压回栈中
  2. 要求 :时间复杂度为 O(N),空间复杂度为 O(N)

思路遍历 tokens 向量,借助一个栈来实现:

  • 遇到数字 → 入栈
  • 遇到操作符 → 先从栈中弹出右操作数,再弹出左操作数
  • 根据操作符进行计算,将结果再压入栈中
  • 遍历结束后,栈顶元素就是表达式的最终值

操作

可以使用范围 for 循环 遍历逆波兰表达式: for(auto& str : tokens){ }


  • 遇到数字就入栈

    cpp 复制代码
      else
      {
          st.push(stoi(str));	
      }
    • stoi(str) 将字符串数字转为整型

  • 遇到操作符,取出栈里的两个数字并计算:

    • 注意:先弹出的是右操作数

    cpp 复制代码
      if(str == "+" || str == "-" || str == "*" || str == "/")
      {
          int right = st.top();
          st.pop();
      
          int left = st.top();
          st.pop();
      }

  • 根据不同操作符进行对应运算,并把运算结果重新入栈:

    cpp 复制代码
      switch(str[0])
      {
          case '+':
              st.push(left + right);
              break;
          case '-':
              st.push(left - right);
              break;
          case '*':
              st.push(left * right);
              break;
          case '/':
              st.push(left / right);
              break;
      }

  • 遍历结束后,最终结果在栈顶:

    cpp 复制代码
      return st.top();
  • 最终返回计算结果,为一个整数


代码实现

cpp 复制代码
class Solution
{
public:
    int evalRPN(vector<string>& tokens) 
    {
        stack<int> st;
        for(auto& str : tokens)
        {
            // 遇到操作数就入栈
            // 遇到操作符就取出栈中的 两个 数,进行运算,计算结果也要入栈
            // 注意先取出的是右操作数
            if(str == "+" || str == "-" || str == "*" || str == "/")
            {
                int right = st.top();
                st.pop();

                int left = st.top();
                st.pop();

                // 是什么操作符,就进行什么运算,运算结果入栈
                switch(str[0])
                {
                    case '+':
                        st.push(left + right);
                        break;
                    case '-':
                        st.push(left - right);
                        break;
                    case '*':
                        st.push(left * right);
                        break;
                    case '/':
                        st.push(left / right);
                        break;
                }
            }
            // 遇到数字,就入栈
            else
            {
                st.push(stoi(str));
            }
        }
        // 最终计算结果存在 栈顶
        return st.top();
    }
};

算法代码优化

  • 后续会使用map对运算符匹配的逻辑进行优化,后续讲解

结语


以上就是本文的所有内容了,如果觉得文章对你有帮助,欢迎 点赞⭐收藏 支持!如有疑问或建议,请在评论区留言交流,我们一起进步

分享到此结束啦
一键三连,好运连连!

你的每一次互动,都是对作者最大的鼓励!


征程尚未结束,让我们在广阔的世界里继续前行! 🚀

相关推荐
Bin二叉42 分钟前
南京大学cpp复习——第二部分(继承)
开发语言·c++·笔记·学习
机器学习之心43 分钟前
198种组合算法+优化TCN时间卷积神经网络+SHAP分析+新数据预测+多输出!深度学习可解释分析,强烈安利,粉丝必备!
深度学习·算法·shap分析·tcn时间卷积神经网络
代码游侠44 分钟前
数据结构——线性表
linux·c语言·数据结构·学习·算法
吃着火锅x唱着歌1 小时前
LeetCode 3371.识别数组中的最大异常值
数据结构·算法·leetcode
元亓亓亓1 小时前
LeetCode热题100--74. 搜索二维矩阵--中等
算法·leetcode·矩阵
lingggggaaaa1 小时前
免杀对抗——C2远控篇&PowerShell&C#&对抗AV-EDR&停用AMSI接口&阻断ETW跟踪&调用
c语言·开发语言·c++·学习·安全·c#·免杀对抗
zzzsde1 小时前
【C++】异常:概念及使用
开发语言·c++·算法
繁星星繁1 小时前
CMake快速上手
c语言·c++·编辑器·学习方法·visual studio code
·醉挽清风·1 小时前
学习笔记—C++—vector
c++·笔记·学习