上次把栈和队列的基础概念过了一遍这次DAY11的part02直接把应用层面拉满。如果你以为栈和队列只是“先进后出、先进先出”两个口诀那接下来的内容可能会让你改观——这两个结构几乎覆盖了笔试面试里一大类经典题型也是后续学习二叉树、图论、动态规划时绕不开的基本功。这次的重点很明确用栈实现队列、用队列实现栈以及括号匹配、删除相邻重复项、逆波兰表达式求值这几道栈的经典应用。别小看这些题它们表面上是“用A模拟B”实际上考察的是你对两个数据结构特性的理解深度以及对接口设计、边界条件的把控能力。这一篇我直接把思路拆开揉碎讲清楚代码用C实现附带分析过程和避坑经验照着走就行。1. 内容整体设计与思路拆解1.1 核心需求解析代码随想录训练营DAY11主题是“第五章 栈与队列part02”明确了三个任务用两个栈实现一个队列力扣232用两个队列实现一个栈力扣225栈在系统与算法中的实际应用场景括号匹配、删除相邻重复项、逆波兰表达式这三个任务其实是一个递进关系前两道题考察的是“用别的结构模拟目标结构”后三道考察的是“在什么场景下应该想到用栈”。从学习路径上看part01讲的是栈和队列的定义、底层实现、STL接口part02则直接进入“怎么用”的阶段。1.2 为什么“模拟”是必考考点很多初学者觉得“用栈模拟队列”这类题很无聊——既然有现成的queue为什么还要用stack去模拟这背后的真实原因是考察你对接口一致性的理解。在实际开发中你经常需要在一个系统里替换底层数据结构。比如项目里原来用std::queue做任务队列后来因为需要支持并发取数、回退操作你要换成其他结构但不能影响上层调用方的使用习惯。这时候“如何保证接口行为不变”就是核心工程能力。换句话说这不仅是算法题更是“接口设计”能力的基础训练。能清晰定义出“哪些操作是核心、哪些操作是辅助”是拆解这类问题的关键。1.3 选题逻辑与学习顺序代码随想录的顺序安排是有讲究的先用力扣232练习“双栈配合”的思维模式理解两个结构如何分工协作再用力扣225练习“双队列轮流倒腾”的思维模式理解数据在主备结构间迁移然后进入栈的实战场景通过括号匹配等题目强化“栈顶即当前状态”的心智模型这个顺序从“结构模拟”到“场景应用”层层递进目标就是帮你建立“看到问题先想想能不能用栈/队列”的条件反射。2. 核心细节解析与实操要点2.1 栈和队列的本质差异回顾在进入代码之前先把底层逻辑理清楚。栈和队列在数据结构上的差异很简单栈只允许在同一端栈顶进行插入和删除后进先出LIFO队列一端入队队尾另一端出队队头先进先出FIFO但真正值得思考的是为什么栈和队列常常被放在一起学因为它们都是“受限的线性表”都只暴露少量接口来操作数据。正因如此它们特别适合用来做“互相模拟”的题目——行为差异越大用另一个结构模拟时越要动脑子。2.2 核心API设计要点模拟结构时光是代码跑通不够还要注意接口设计的一致性。以“用栈实现队列”为例必须实现以下接口push(int x)将元素放入队列尾部pop()从队列头部移除元素并返回peek()返回队列头部元素不移除empty()判断队列是否为空这里有一个关键设计点栈的顶部对应队列的哪个位置我用一句话给新手讲透入队往一个栈里塞出队从另一个栈里倒。“倒”这个动作就是把元素从一个栈全部弹到另一个栈里顺序正好反一次负负得正队头就露出来了。2.3 复杂度分析思路这类题的复杂度分析也比较特殊push时间复杂度O(1)pop均摊时间复杂度O(1)。虽然某些时刻会触发全部元素搬迁但每个元素最多被“倒”两次一次入、一次出均摊下来还是常数级。空间复杂度O(n)需要双份存储空间这个概念在面试时经常被追问答清楚“均摊”和“最坏”的区别是加分项。3. 实操过程与核心环节实现3.1 用两个栈实现队列力扣232这是整个DAY11最核心的一道题。思路如下用两个栈一个叫stackIn专门处理入队一个叫stackOut专门处理出队入队时直接push进stackIn出队时如果stackOut为空先把stackIn全部弹到stackOut再从stackOut弹出栈顶这是个“负负得正”的过程入栈时顺序是逆序搬到另一个栈后再次逆序就变回原序直接上C代码#include stack class MyQueue { private: std::stackint stackIn; std::stackint stackOut; // 将stackIn中的元素全部转移到stackOut void transfer() { while (!stackIn.empty()) { stackOut.push(stackIn.top()); stackIn.pop(); } } public: MyQueue() {} void push(int x) { stackIn.push(x); } int pop() { // 如果stackOut为空先从stackIn中转移 if (stackOut.empty()) { transfer(); } int result stackOut.top(); stackOut.pop(); return result; } int peek() { // 复用transfer逻辑 if (stackOut.empty()) { transfer(); } return stackOut.top(); } bool empty() { return stackIn.empty() stackOut.empty(); } };代码解读与实操心得transfer()被提炼为单独函数这里有个小细节两个栈都需要复用“转移”逻辑提炼出来避免重复代码同时可读性更好。peek()复用了transfer但返回top()而不是pop()。很多人会在这里手滑直接把元素弹出去调试半天才发现。我在实际写代码时习惯把transfer()设计成私有的辅助函数——因为外部调用方根本不需要关心内部是怎么腾挪的这也是“信息隐藏”思想的落地。3.2 用两个队列实现栈力扣225这道题的第一反应可能是参考上一题设计queueIn和queueOut。但仔细想想队列是FIFO你怎么倒腾都不会改变顺序所以不能像栈那样“负负得正”。正确思路是把队尾元素变成队头元素每次出栈时把除最后一个以外的元素全部搬到另一个队列再把最后一个元素弹出。这样剩下的那个队列就是“备份”状态。#include queue class MyStack { private: std::queueint queueMain; std::queueint queueHelper; public: MyStack() {} void push(int x) { queueMain.push(x); } int pop() { // 把queueMain中除队尾外的元素搬到queueHelper while (queueMain.size() 1) { queueHelper.push(queueMain.front()); queueMain.pop(); } int result queueMain.front(); queueMain.pop(); // 交换两个队列的角色 std::swap(queueMain, queueHelper); return result; } int top() { // 复用pop逻辑但要把元素放回去 int result pop(); queueMain.push(result); return result; } bool empty() { return queueMain.empty() queueHelper.empty(); } };这段代码里的几个关键点pop()用size() 1作为循环条件避免多搬一个元素。新手常见的错误是搬多了导致原队尾也跑到helper里结果弹出的是错误元素。弹出后交换两个队列的角色这一步让queueMain始终持有当前“栈内”的数据queueHelper始终是空的备用结构。不交换的话下一次pop还要处理残留数据逻辑会乱。top()调用完pop()后把元素再塞回去这个技巧看起来取巧但实际工程里“复用已有逻辑回补状态”是很常见的操作省代码且不容易出bug。3.3 栈的经典应用括号匹配力扣20模拟题做完进入栈的真实应用场景。括号匹配的核心思想是遇到左括号就入栈遇到右括号就检查栈顶是不是匹配的左括号。因为括号的嵌套关系天然是“后出现的先闭合”正好匹配栈的LIFO特性。#include stack #include string class Solution { public: bool isValid(std::string s) { std::stackchar st; for (char c : s) { if (c ( || c [ || c {) { st.push(c); } else { if (st.empty()) { return false; // 右括号但没有左括号可匹配 } char top st.top(); if ((c ) top ! () || (c ] top ! [) || (c } top ! {)) { return false; } st.pop(); } } return st.empty(); // 栈空说明全部匹配 } };解题心得看到c是右括号时先判断栈是否为空。很多解法直接st.top()遇到第一个字符就是}时直接运行时错误。最后一定要return st.empty()而不是return true。如果输入是((()遍历完栈里还有元素应该返回false。这里用if-else判断需要匹配三个类型。更巧的写法是遇到左括号时把对应的右括号入栈这样判断就统一为c st.top()但可读性略差。应试时我倾向于用最不容易出错的写法。3.4 删除字符串中的所有相邻重复项力扣1047这道题的核心启发点在于它让我们看到栈不只用来匹配“成对符号”还能用来消除“相邻冲突”。思路很简单遍历字符串如果当前字符和栈顶相同就弹出栈顶否则入栈。等于每次只盯“栈顶”这一个状态。#include stack #include string class Solution { public: std::string removeDuplicates(std::string s) { std::stackchar st; for (char c : s) { if (!st.empty() st.top() c) { st.pop(); } else { st.push(c); } } std::string result; while (!st.empty()) { result st.top(); st.pop(); } // 字符串顺序需要反转 std::reverse(result.begin(), result.end()); return result; } };避坑注意这里“相邻”是动态的。比如字符串abbaca删除bb后a和a变成相邻也要删除最终结果是ca。栈天然能处理这种“连锁反应”因为你永远只关注栈顶和当前字符。拼接字符串时从栈顶依次取出是逆序需要reverse一下。如果你用result c result;也可以但字符串拼接性能较差。我的做法是最后统一反转效率更好。3.5 逆波兰表达式求值力扣150逆波兰表达式后缀表达式可能是栈应用里最“工程化”的一道题。它把操作数放前面、操作符放后面正好让计算机用栈就能轻松求值不需要处理括号优先级。#include stack #include vector #include string class Solution { public: int evalRPN(std::vectorstd::string tokens) { std::stackint st; for (const std::string token : tokens) { if (token || token - || token * || token /) { int b st.top(); st.pop(); int a st.top(); st.pop(); if (token ) st.push(a b); else if (token -) st.push(a - b); else if (token *) st.push(a * b); else if (token /) st.push(a / b); } else { st.push(std::stoi(token)); } } return st.top(); } };这个题几个值得说透的细节出栈后的两个数b先出栈是右操作数a后出栈是左操作数。除法尤其要注意顺序a / b和b / a结果完全不同。用std::stoi把字符串转整数注意不是atoiC的atoi接受const char*需要额外调用c_str()。题目保证表达式合法所以不用额外判断栈空或除零。但实战中遇到可能非法的输入记得要补防御逻辑。4. 常见问题与排查技巧实录这一部分直接汇总我在实际刷题、带学员过程中遇到的高频问题整理成方便对照的排查表每一个都是真实踩过的坑。症状可能原因排查与解法用栈模拟队列时pop返回错误元素stackOut中残留了旧数据又继续transfer转移前必须确保stackOut为空否则新元素会压在旧元素上层顺序错乱队列模拟栈时pop后栈内数据丢失忘记交换queueMain和queueHelperpop后数据都在helper里必须swap回来否则下一次pop操作对象是空的括号匹配遇到)]}开头时崩溃未判空直接st.top()操作前必须检查st.empty()空栈直接返回false删除相邻重复项结果顺序颠倒从栈顶拼接字符串是逆序拼接完成后统一执行reverse(result.begin(), result.end())逆波兰表达式的除法结果不对左右操作数顺序写反先弹b右操作数再弹a左操作数除法用a / b使用queue的back()接口时报错标准库queue不支持back()的修改操作std::queue的back()返回的是引用但某些自定义版本不一定支持用push到helper的方式更通用4.1 专属避坑建议除了上表还有几条我在实际写代码时的习惯操作分享出来第一优先用STL而不是手写底层结构。代码随想录的讲解里也会提到力扣平台上用std::stack、std::queue完全够用重点考察的是思路。手写数组模拟栈也不是不行但没必要给自己增加调试成本。第二写模拟题时画状态图。我用双栈模拟队列时最常犯的错是忘了stackOut里还有数据就去transfer。后来养成了习惯写代码前先在草稿纸上画出两个栈的内容然后一步步推演push、pop的执行过程。画完一遍代码边写边检查基本不会再出现逻辑错乱。第三所有辅助方法先写在private区域。把transfer这类逻辑封装成私有函数主流程代码会变得很干净。面试时这种代码风格很加分它体现出你具备基本的抽象能力。第四C里用std::swap交换两个容器是O(1)。很多人以为swap要遍历元素其实标准库的swap对容器特化过只是交换内部指针非常快。用这个操作能简化大量逻辑。5. 栈与队列背后的工程思想刷完这几道题除了“会做题”之外我更想让你看到栈和队列在工程上的底层价值。5.1 函数调用与递归每次函数调用系统都会在调用栈上压入一个栈帧保存局部变量、返回地址、参数。递归函数为什么会栈溢出因为每一层递归都往系统栈里压帧压得太深就爆了。这就是栈在操作系统层面的直接体现。理解了这一点你就明白为什么递归转迭代时经常要手动维护一个栈——你在模拟系统帮你做的事。5.2 消息队列与解耦队列在工程里的角色更像是“缓冲区”和“解耦器”。消息队列比如RabbitMQ、Kafka的本质就是两个进程之间塞了一个队列生产者和消费者不必同时在线也不必互相等待。代码随想录学到这里很多学员才真正理解“阻塞队列”是什么——它就是一个线程安全、支持等待通知机制的队列。你在操作系统课程里学到的生产者消费者模型本质也是在用队列这个结构。5.3 撤销操作与浏览器历史编辑器的“撤销”、浏览器的“后退”都是典型的栈应用。你每做一次操作就入栈一次撤销时弹出栈顶。浏览器的前进后退则更复杂一点需要双栈配合——这和“用两个栈实现队列”的思想有着异曲同工之处。6. 后续学习路径建议DAY11结束栈与队列的标准题型还没有完全穷尽。这里给你三条可执行的后续建议优先攻克单调栈力扣739每日温度、496下一个更大元素I、42接雨水。这些是栈的进阶应用核心思路是维护栈内元素的单调性常考且需要额外的理解成本。把队列用于BFS二叉树层序遍历、图的广度优先搜索都会用队列做“逐层扩散”。这是后面图论题的基石建议提前熟悉。动手做一个“计算器”小项目把中缀表达式转后缀表达式逆波兰再用栈求值。这个项目能把今天学的栈应用串成一条线做完你会对“表达式求值”有完整的认识。我在代码随想录训练营里带过几百个学员观察到一个很明显的分水岭能独立完成DAY11这三道实战题的人后面学二叉树时普遍轻松很多。原因很简单——栈和队列教会你的不是某个具体题目的解法而是“数据从哪里来、到哪里去、什么时候临时存一下”这种抽象思维能力。这个能力才是算法学习中最值钱的部分。这些天写代码写累了我习惯用之前提到的第三点建议——画状态图来复盘今天的代码。每次画完都觉得数据结构其实并不神秘无非是给数据立了几条规矩然后看你在规矩里能玩出什么花来。栈和队列的规矩已经够用了接下来就看你怎么用它们解决更复杂的问题。