Leetcode栈与队列

232. 用栈实现队列

解题思路:用两个栈模拟队列

1. 问题背景

  • 队列(Queue)是先进先出(FIFO)的数据结构:先入队的元素先出队。
  • (Stack)是后进先出(LIFO)的数据结构:最后压入的元素最先弹出。
  • 目标:仅使用两个栈 (以及栈的标准操作:push, pop, top, empty

2. 核心观察

  • 单个栈无法实现 FIFO,但两个栈配合可以反转顺序两次,从而还原 FIFO 行为。
  • 具体策略:
    • 入队push):所有新元素都压入 输入栈inStack)。
    • 出队pop/peek):从 输出栈outStack)弹出。
      • 如果 outStack 为空,则把 inStack所有元素一次性倒过来 压入 outStack
      • 这样,最早入队的元素就会出现在 outStack 的顶部!

3. 为什么有效?

  • 第一次入栈(inStack):顺序是 [1, 2, 3](1 最早入队)。
  • 倒入 outStack 后:变成 [3, 2, 1],此时 top()1 ------ 正好是队首!
  • 后续出队:1 → 2 → 3,符合 FIFO。

4. 关键优化:延迟转移(Lazy Transfer)

  • 不在每次 push 时转移数据,而只在 outStack 为空且需要出队时才转移。

  • 这样保证了均摊时间复杂度为 O(1)

    class MyQueue {
    private:
    stack<int> inStack; // 用于入队
    stack<int> outStack; // 用于出队

    复制代码
      // 辅助函数:将 inStack 的所有元素转移到 outStack
      void transfer() {
          while (!inStack.empty()) {
              outStack.push(inStack.top()); // 把 inStack 顶部元素压入 outStack
              inStack.pop();                // 弹出 inStack 顶部
          }
      }

    public:
    MyQueue() {
    // 构造函数留空即可
    }

    复制代码
      // 入队:直接压入 inStack
      void push(int x) {
          inStack.push(x);
      }
    
      // 出队:若 outStack 为空,先转移;然后弹出栈顶
      int pop() {
          if (outStack.empty()) {
              transfer(); // 延迟转移
          }
          int val = outStack.top();
          outStack.pop();
          return val;
      }
    
      // 查看队首元素:逻辑同 pop,但不弹出
      int peek() {
          if (outStack.empty()) {
              transfer();
          }
          return outStack.top();
      }
    
      // 判断队列是否为空:两个栈都为空才算空
      bool empty() {
          return inStack.empty() && outStack.empty();
      }

    };

225. 用队列实现栈

解题思路:用两个队列实现栈

核心目标

  • 栈是 LIFO(后进先出)
  • 队列是 FIFO(先进先出)
  • 要用 FIFO 结构模拟 LIFO 行为。

常见策略(两种主流方法)

✅ 方法 1:压入时调整顺序(推荐,更高效)
  • 始终保持 que1 中的元素顺序是"栈顶在队首"。
  • 每次 push(x) 时:
    1. 先把 x 放入空队列(比如 que2
    2. 再把 que1 所有元素移到 que2
    3. 交换 que1que2
  • 这样 que1.front() 始终是栈顶。
  • 优点pop()top() 都是 O(1)
⚠️ 方法 2:弹出时调整顺序(采用的方法)
  • push 直接入 que1
  • pop/top 时,把前 n-1 个元素移到 que2,留下最后一个(即栈顶)
  • 然后再把 que2 搬回 que1
  • 缺点 :每次 pop/top 都是 O(n),且频繁拷贝/清空队列,效率较低

代码属于 方法 2

复制代码
class MyStack {
private:
    queue<int> q1, q2;

public:
    void push(int x) {
        q1.push(x);
    }

    int pop() {
        // 将 q1 前 n-1 个移到 q2
        while (q1.size() > 1) {
            q2.push(q1.front());
            q1.pop();
        }
        int res = q1.front();
        q1.pop();
        swap(q1, q2); // 交换后,q1 是主队列,q2 为空(无需手动清空!)
        return res;
    }

    int top() {
        // 同 pop,但要把最后一个也移到 q2
        while (q1.size() > 1) {
            q2.push(q1.front());
            q1.pop();
        }
        int res = q1.front();
        q2.push(res);   // 保留栈顶元素
        q1.pop();
        swap(q1, q2);
        return res;
    }

    bool empty() {
        return q1.empty();
    }
};

20. 有效的括号

解题思路:用栈匹配括号

问题要求

给定一个只包含 '(', ')', '{', '}', '[', ']' 的字符串,判断其是否有效。有效需满足:

  1. 左右括号必须成对出现;
  2. 括号必须以正确的顺序闭合(不能 ([)]);
  3. 所有左括号都有对应的右括号,反之亦然。

核心思想:栈的后进先出(LIFO)

  • 遇到左括号(, {, [),就把对应的右括号压入栈;
  • 遇到右括号 ,就检查:
    • 栈是否为空?→ 空说明没有匹配的左括号 ❌
    • 栈顶是否等于当前右括号?→ 不等说明类型不匹配 ❌
    • 相等则弹出栈顶 ✅
  • 遍历结束后,栈必须为空(所有左括号都被匹配)

💡 为什么压入"右括号"而不是左括号?

  • 这样在遇到右括号时,可以直接比较 s[i] == st.top(),无需额外映射。
  • 是一种巧妙的编码技巧,提升可读性。
复制代码
class Solution {
public:
    bool isValid(string s) {
        // 优化:奇数长度的字符串一定无效(括号必须成对)
        if (s.size() % 2 != 0) return false;

        stack<char> st;

        for (int i = 0; i < s.size(); i++) {
            // 遇到左括号,压入对应的右括号
            if (s[i] == '(') st.push(')');
            else if (s[i] == '{') st.push('}');
            else if (s[i] == '[') st.push(']');

            // 遇到右括号(或非法字符)
            else {
                // 情况1:栈空 → 右括号无匹配左括号
                // 情况2:栈顶 ≠ 当前字符 → 括号类型不匹配
                if (st.empty() || st.top() != s[i])
                    return false;

                // 匹配成功,弹出栈顶
                st.pop();
            }
        }

        // 遍历结束:栈空 → 所有括号都匹配;栈非空 → 有多余左括号
        return st.empty();
    }
};

1047. 删除字符串中的所有相邻重复项

解题思路:用栈模拟"消除相邻重复"

问题要求

  • 给定一个字符串 S反复删除相邻且相同的两个字符,直到无法再删。
  • 例如:
    • "abbaca" → 删除 "bb""aaca" → 删除 "aa""ca"
    • "abba""aa"""

核心观察

  • 消除是连锁反应的:删除一对后,前后字符可能又变成相邻重复。
  • 这种"后进先出 + 回退匹配"的行为,天然适合用栈

算法策略

  1. 遍历字符串每个字符 c
    • 如果栈为空,或 c ≠ 栈顶 → 压入 c
    • 如果 c == 栈顶 → 弹出栈顶(相当于消除这对)
  2. 遍历结束后,栈中剩下的字符就是最终结果(但顺序是反的)
  3. 将栈中元素倒出并反转,得到答案

💡 为什么栈能处理连锁消除?

  • 因为每次消除后,新的栈顶就是"当前最后一个未被消除的字符",正好与下一个字符比较。
复制代码
string removeDuplicates(string S) {
    stack<char> st;
    for (char s : S) {
        if (st.empty() || s != st.top()) {
            st.push(s);      // 不重复,压入
        } else {
            st.pop();        // 重复,消除(弹出栈顶)
        }
    }

    string result = "";
    while (!st.empty()) {
        result += st.top();  // 从栈顶取出(顺序是反的)
        st.pop();
    }
    reverse(result.begin(), result.end()); // 反转得到正确顺序
    return result;
}

150. 逆波兰表达式求值

解题思路:用栈计算逆波兰表达式

什么是逆波兰表达式?

  • 操作符在操作数之后 ,例如:
    • 中缀:(2 + 1) * 3 → 后缀:["2", "1", "+", "3", "*"]
  • 优点:无需括号,天然适合用栈计算。

计算规则

  1. 遇到数字 → 压入栈;
  2. 遇到运算符+, -, *, /)→
    • 弹出两个数字(注意顺序!)
    • 先弹出的是右操作数,后弹出的是左操作数
    • 计算 左 op 右,结果压回栈
  3. 遍历结束后,栈中只剩一个数 → 即为结果

🔑 关键细节:减法和除法不满足交换律 ,必须注意顺序!

例如:["4", "2", "/"] 表示 4 / 2 = 2,不是 2 / 4

复制代码
int evalRPN(vector<string>& tokens) {
    stack<long long> st;
    for (int i = 0; i < tokens.size(); i++) {
        // 判断是否为运算符
        if (tokens[i] == "+" || tokens[i] == "-" || 
            tokens[i] == "*" || tokens[i] == "/") {
            
            long long tmp1 = st.top(); st.pop(); // 右操作数(后压入,先弹出)
            long long tmp2 = st.top(); st.pop(); // 左操作数

            if (tokens[i] == "+") st.push(tmp2 + tmp1);
            if (tokens[i] == "-") st.push(tmp2 - tmp1); // ✅ 正确:左 - 右
            if (tokens[i] == "*") st.push(tmp2 * tmp1);
            if (tokens[i] == "/") st.push(tmp2 / tmp1); // ✅ 正确:左 / 右
        }
        else {
            // 数字字符串转 long long
            st.push(stoll(tokens[i]));
        }
    }
    return st.top(); // 栈顶即结果
}

239. 滑动窗口最大值

解题思路:用单调队列维护滑动窗口最大值

问题目标

给定数组 nums 和窗口大小 k,返回每个长度为 k 的滑动窗口中的最大值

暴力法 vs 优化法

  • 暴力法:每个窗口遍历找最大值 → 时间复杂度 O(nk),超时 ❌
  • 单调队列法 :维护一个递减双端队列 ,队首始终是当前窗口最大值 → O(n)

单调队列的核心思想

我们维护一个双端队列 deque,其中:

  • 存储的是数组元素的值(你这里存的是值,也可以存索引------稍后对比)
  • 队列从队首到队尾严格递减(或非递增)
  • 队首 = 当前窗口最大值
两个关键操作:
  1. push(value)
    • 在插入新元素前,从队尾弹出所有 ≤ 当前值的元素
    • 这样保证队列单调递减,且新元素"淘汰"了不可能成为最大值的旧元素
  2. pop(value)
    • 只有当被移出窗口的元素恰好是当前最大值(即队首)时,才从队首弹出
    • 否则,它早已在之前的 push 中被移除,无需处理

💡 为什么可以"忽略非最大值的过期元素"?

因为它们在入队时就被更大的元素"挤掉"了,根本不在队列中!

复制代码
class Myque {
public:
    deque<int> que;

    // 移除窗口左边移出的元素(仅当它是当前最大值时)
    void pop(int value) {
        if (!que.empty() && que.front() == value) {
            que.pop_front();
        }
    }

    // 插入新元素,维护单调递减
    void push(int value) {
        while (!que.empty() && value > que.back()) {
            que.pop_back(); // 弹出队尾小于当前值的元素
        }
        que.push_back(value);
    }

    int front() {
        return que.front(); // 队首即当前窗口最大值
    }
};

vector<int> maxSlidingWindow(vector<int>& nums, int k) {
    Myque que;
    vector<int> result;

    // 初始化第一个窗口 [0, k-1]
    for (int i = 0; i < k; i++) {
        que.push(nums[i]);
    }
    result.push_back(que.front());

    // 滑动窗口:[i-k+1, i]
    for (int i = k; i < nums.size(); i++) {
        que.pop(nums[i - k]);     // 移除窗口左边界的元素
        que.push(nums[i]);        // 加入新元素
        result.push_back(que.front());
    }
    return result;
}

347. 前 K 个高频元素

解题思路:哈希 + 最小堆(Top-K 问题模板)

步骤分解:

  1. 统计频次 :用 unordered_map<int, int> 记录每个数字的出现次数。
  2. 维护大小为 k 的最小堆
    • 堆中存储 (数字, 频次) 对;
    • 堆顶是当前 k 个元素中频次最小的
    • 当堆大小 > k 时,弹出堆顶(淘汰频次最小的);
  3. 最终堆中就是 top-k 高频元素
  4. 倒序输出(因为最小堆弹出顺序是从小到大,而题目不要求顺序,但你选择从后往前填)。

✅ 这种方法比"全排序"(O(n log n))更优,尤其当 k << n 时。

复制代码
class Solution {
public:
    // 修正拼写:mycomparison
    class mycomparison {
    public:
        bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) {
            return lhs.second > rhs.second; // 构建最小堆:堆顶是频次最小的
        }
    };

    vector<int> topKFrequent(vector<int>& nums, int k) {
        unordered_map<int, int> map;
        for (int num : nums) {
            map[num]++;
        }

        // 定义最小堆(大小为 k)
        priority_queue<
            pair<int, int>,
            vector<pair<int, int>>,
            mycomparison
        > prique;

        for (auto& kv : map) {
            prique.push(kv);
            if (prique.size() > k) {
                prique.pop(); // 弹出频次最小的
            }
        }

        // 从堆中取出结果(顺序无所谓,但你选择倒序填充)
        vector<int> result(k);
        for (int i = k - 1; i >= 0; i--) {
            result[i] = prique.top().first;
            prique.pop();
        }
        return result;
    }
};
相关推荐
2501_944525549 小时前
Flutter for OpenHarmony 个人理财管理App实战 - 预算详情页面
android·开发语言·前端·javascript·flutter·ecmascript
zhuqiyua9 小时前
第一次课程家庭作业
c++
9 小时前
java关于内部类
java·开发语言
好好沉淀9 小时前
Java 项目中的 .idea 与 target 文件夹
java·开发语言·intellij-idea
只是懒得想了9 小时前
C++实现密码破解工具:从MD5暴力破解到现代哈希安全实践
c++·算法·安全·哈希算法
lsx2024069 小时前
FastAPI 交互式 API 文档
开发语言
VCR__9 小时前
python第三次作业
开发语言·python
码农水水9 小时前
得物Java面试被问:消息队列的死信队列和重试机制
java·开发语言·jvm·数据结构·机器学习·面试·职场和发展
wkd_0079 小时前
【Qt | QTableWidget】QTableWidget 类的详细解析与代码实践
开发语言·qt·qtablewidget·qt5.12.12·qt表格
东东5169 小时前
高校智能排课系统 (ssm+vue)
java·开发语言