栈与队列实战指南:从双栈模拟到单调队列与工程应用

发布时间:2026/10/22 10:42:58
栈与队列实战指南:从双栈模拟到单调队列与工程应用 1. 先搞清楚栈和队列到底在解决什么问题很多朋友学到栈和队列这里会觉得“这么简单的东西有什么好学的”。数组、链表都玩得转一个先进后出、一个先进先出无非是两种访问规则。但等你真正开始刷题尤其是刷到“用栈实现队列”“滑动窗口最大值”这种题的时候才会发现数据结构本身的逻辑线只有一条但应用场景千变万化每一种变形背后都藏着一种算法思维。我在训练营带过不少同学大家最容易踩的坑不是不理解栈和队列的定义而是“不知道什么时候该用栈、什么时候该用队列”。拿到一道题脑子里先蹦出来的全是暴力解法遍历一遍再遍历一遍循环套循环最后超时了才想起来还有更优的思路。所以这一篇实战总结我打算换个讲法。不是把每道题的代码贴一遍就完事而是带着你从题目入手先分析“为什么能想到用栈/队列”再动手实现最后把最容易出错的地方全部挑出来说清楚。这才是我认为“实战”两个字真正的意义。毕竟算法题面试的时候考官最看重的不是你能不能默写某道题的答案而是你面对一个没见过的题目时能不能通过数据结构的选择把复杂度降下来把逻辑理清楚。2. 经典题目逐题拆解从思路到代码的完整推导过程2.1 用栈实现队列双栈模拟的核心思想题目是这样的仅使用两个栈实现先入先出队列支持队列的基本操作push、pop、peek、empty。我第一次看到这道题的时候也愣了一会儿。栈是后进先出队列是先进先出一根轴倒不过来那就用两根轴。一根栈负责“入队”另一根栈负责“出队”。入队的时候只管往第一个栈里压出队的时候先把第一个栈里的元素全部倒进第二个栈这时候第二个栈的栈顶就是最早进来的元素相当于把顺序翻转了一次。这里有一个关键点不需要每次出队都倒一次数据。只需要在第二个栈为空的时候才倒否则直接取第二个栈的栈顶就行。这样可以保证每个元素最多被“倒”两次均摊时间复杂度是O(1)。实际写代码的时候需要注意两个边界pop的时候如果两个栈都为空要抛出异常peek的逻辑和pop基本一样只是不弹出栈顶。实现起来并不复杂但这是设计层面的思维训练——用现成的数据结构拼出另一种语义。2.2 用队列实现栈一个队列也能行两个队列更容易理解反过来用队列实现栈就是先进先出要伪装成后进先出。如果你有两个队列思路很直观入栈的时候先把新元素放进空的队列然后把另一个队列里的所有元素依次搬过来这样新元素就在队头了队头也就是栈顶。出栈就是直接弹出队头。但如果你只想用一个队列思路会更有趣。每次入栈时先把元素放进队尾然后循环把队尾之前的元素全部弹出再重新入队这样新元素就被旋转到队头。循环的次数是当前队列长度减一。这种方法的时间复杂度是O(n)在数据量小的时候完全够用。很多人在写这道题的时候容易绕晕尤其是单队列版本。我建议自己动手画几下队列从头到尾的方向画一个横向箭头每一步元素怎么移动、谁变成了队头画完你就不会再迷了。2.3 有效的括号栈匹配问题背后的计算机历史输入一串只包含括号字符的字符串判断括号的闭合顺序是否正确。这题在LeetCode上属于easy级别但它的实际意义远远不只面试。为什么这道题用栈是最自然的解法因为计算机处理嵌套结构本质上就是栈式的。编译原理里做词法解析和语法解析括号匹配、标签配对、作用域判断全都离不开一个“符号栈”。你在编辑器里写代码花括号折起来不提示错误就是基于这样的机制。解法并不复杂遇到左括号就压栈遇到右括号就检查栈顶是不是对应的左括号是就弹出不是就说明匹配失败。字符串遍历完之后栈要为空才说明所有括号都闭合了。但这里有几个容易忽略的地方。第一如果在匹配过程中栈已经空了说明右括号多余直接返回false。第二遍历结束后栈不为空说明有左括号没有被闭合也是false。第三可以用哈希表建立左右括号的映射关系简化代码里的if-else链。这道题真正的价值在于它让你理解为什么很多解析器要用“栈”来维护状态。有了这个基础你以后遇到类似“标签闭合”“HTML解析”“撤销操作”这类问题思路会一下子打开。2.4 滑动窗口最大值单调队列的实战这道题比前面几道难上一个台阶。给定一个整数数组和一个窗口大小k窗口每次向右移动一格请输出窗口内每个位置的最大值。最直观的办法是每个窗口遍历一遍找最大值。假设数组长度是n这种方法的时间复杂度是O(n×k)在n很大而k又不小的时候超时几乎是必然的。要想把复杂度降到O(n)就需要引入单调队列——队列中的元素在某种意义上是“单调递减”的队头永远是当前窗口的最大值候选。关键在入队操作当新元素要进入队列时把队列尾部所有比它小的元素全部弹出然后再入队。这样一来队头到队尾是按值递减的。同时还需要检查队头的下标是否已经滑出窗口如果滑出了就弹出队头。这样每次窗口滑动时队头元素就是当前窗口的最大值复杂度均摊下来是O(1)。很多同学第一次写单调队列时会犯一个错误只维护了值没有维护下标。结果窗口滑动之后队头元素明明是上一个窗口的却被当作当前窗口的最大值返回了。这里必须记住队列里存的是元素的下标而不是元素的值。通过下标可以快速判断是否过期也可以随时访问对应值。2.5 逆波兰表达式求值栈与表达式解析的完美配合逆波兰表达式也叫后缀表达式是一种把运算符写在操作数之后的表示法。比如“3 4 ”就表示3加4。为什么要研究这种看起来反人类的表达式因为在计算机看来这种表达式不需要考虑括号优先级可以严格从左到右扫描用栈就能轻易求值。经典的计算器程序往往先把用户输入的中缀表达式转为后缀再通过栈来计算结果。具体做法很简单从左到右扫描表达式遇到数字就压栈遇到运算符就弹出栈顶的两个数字计算结果后再压回栈。表达式扫描完栈里剩下的唯一数字就是答案。但细节上有很多坑。第一个坑是数字和运算符在输入中被表示成字符串需要做类型判断。第二个坑是减法除法时两个操作数的顺序不能搞反——先弹出的是右操作数后弹出的是左操作数。第三个坑是除法涉及负数的情况在大多数语言里整数除法是向零取整的这一点在LeetCode这题里也有明确约定。3. 容器选择分析不同语言的底层差异与应对3.1 开发环境与语言选型在训练营里我统一建议用自己最熟悉的语言来刷题但对栈和队列这种结构语言本身提供的容器特性会影响实现方式。以Go为例标准库中并没有直接提供“栈”或“队列”这样的高级结构。切片slice天然适合模拟栈因为可以在末尾进行push和pop操作时间复杂度都是O(1)。队列则可以用切片模拟通过维护起始下标来避免频繁的元素搬移否则每次从头部删除元素会导致后面的元素全部前移效率极低。另一种方案是用容器双向链表container/list来模拟队列。双向链表的头部和尾部操作都是O(1)复杂度写起来也非常清晰。但它有一个问题节点存储的是空接口类型出队时要做一次类型断言代码上略显繁琐性能上也会有小幅折损。3.2 切片的动态扩容与效率陷阱使用切片模拟栈时反复的追加操作会触发扩容机制。Go的切片扩容策略是容量不够时自动重新分配底层数组并将原有元素复制过去。这个复制过程的时间复杂度表面上看起来不是常量的但由于扩容次数是指数递减的均摊下来依然是O(1)。在实际刷题中大多数情况下不需要关心切片扩容带来的性能损失。但如果你在做一个高性能组件对性能极端敏感可以预先用make初始化一个有足够容量的切片避免多次扩容带来的拷贝开销。以滑动窗口最大值那道题为例你可以预分配一个长度为n的切片来充当队列然后手动管理队头和队尾下标这样既能避免动态扩容又能保证O(1)的访问速度工程上很实用。3.3 各语言容器对比速查语言栈模拟方式队列模拟方式注意事项Go切片末尾追加/弹出切片维护下标或双向链表头部删除要避免频繁搬移Python列表append/popcollections.dequedeque的popleft是O(1)JavaArrayDeque / StackArrayDeque / LinkedList不推荐用Stack类效率低Cvector back/push/popqueue容器适配器queue默认底层deque4. 从刷题到应用栈与队列的真实应用场景分析4.1 浏览器后退与前进双栈的经典应用浏览器的后退按钮就是“用两个栈模拟队列”的同门师兄弟。你访问页面A、B、C每访问一个新页面就把它压入后退栈。当你点了后退从B回到A时B会被弹出并压入前进栈。如果此时你访问了一个新页面D前进栈需要清空因为旧的“前进记录”已经失效。这个逻辑很多人以为是浏览器内部复杂的管理系统但本质上它就是两个栈的操作。理解了双栈的弹出、压入、清空规则你就能写出一个非常像样的浏览器历史管理器。这个例子也印证了一个观点算法的价值从来不是只活在刷题网站里而是无处不在。4.2 函数调用栈与递归的关系程序的每一次函数调用底层的调用栈都会压入一个“栈帧”里面保存了函数的局部变量、参数和返回地址。函数返回时栈帧弹出控制权交还给上一层。这也是“栈”这一名称的由来——它真的是一个栈结构。理解这一点对掌握递归至关重要。每一层递归调用都会形成新的栈帧递归深度过高会耗尽栈空间导致栈溢出。很多同学写递归的时候没想明白这个机制总以为递归是“魔法”其实它只是函数调用自己同样受调用栈深度的限制。4.3 打印任务缓冲与消息队列队列在计算机系统中的角色更为普遍。打印任务总是先提交先服务网络数据包到达网卡后被排队处理多个线程往同一个日志文件里写内容也要经过一个缓冲队列来排队。可以说操作系统的任务调度、进程通信、消息队列框架都是队列思想的直接体现。在你写出“用队列实现栈”的代码时不妨想一想真实的系统里为什么很多地方宁可牺牲一些效率也要保证先进先出的公平性这是因为在并发场景下“按到达顺序处理”是一种最简单也最不容易出错的设计。5. 易错点总结与避坑清单这部分我只写踩过坑的常规教程里不会给你标注得这么细。5.1 判空逻辑不清导致崩溃无论是用栈实现队列还是用队列实现栈都必须有明确的判空条件。很多同学在实现pop或top操作时默认“调用者一定保证不为空”而实际面试官给出的测试用例里一定会包含对一个空结构进行pop的场景。如果你没有处理空的情况直接访问栈顶或队头轻则得到错误结果重则程序崩溃。养成习惯在任何pop、peek、top操作的前面先判断结构是否为空。5.2 下标滑动窗口中的过期元素滑动窗口最大值这个题几乎所有人都能在“维护单调队列”这个思路上理解但真正写起来就会漏掉“过期元素弹出”。原因是写代码的时候你把注意力全部放在了“保持队内单调递减”上忘了窗口是在滑动的。这里我的习惯是每轮循环开始的时候先检查队头下标是否等于当前位置减去k如果等于说明它已经滑出窗口弹出它。这一步和“新元素入队时弹出队尾较小值”是两个独立操作缺一不可。5.3 逆波兰表达式中操作数顺序逆波兰表达式里减法除法的顺序问题我见得太多了。比如表达式“10 5 -”正确结果是5但如果你写成了先弹出的数字减去后弹出的数字就会得到-5。关键在于理解栈是先入后出的先弹出的是后入栈的元素也就是表达式里靠右边的操作数。在运算符处理逻辑里应该是left : pop()是后弹出的数字right : pop()是先弹出的数字。为了不绕我建议代码里写成left和right两个变量注释里直接说明含义。6. 训练营实战中的调试技巧与编码习惯6.1 手玩数据判空法与打印辅助法我特别推荐一个笨但有效的方法遇到栈和队列的题目先在草稿纸上手动模拟几组数据每一步都写下来。比如用队列实现栈题目中“push(1) push(2) pop() push(3) pop()”这个操作序列你的队列每一步里应该有哪些元素从队头到队尾怎么排全部画出来。然后才是写代码。写完再用同样的数据跑一遍逐行把队列和栈的状态打印出来和手玩的结果对比。这个习惯能帮你把“思路不清”和“代码写错”混在一起的问题彻底剥离开。6.2 边界输入优先测试习惯很多人刷题有一个坏习惯写完代码直接提交通过了就万事大吉没通过就对着用例瞪眼。更好的做法是在提交之前先自己构造几类边界用例空结构操作、只有一个元素的队列、元素全部相同、窗口大小等于数组长度等。用一个最小的输入集合把代码里所有分支都走到再提交。这样能减少大量无意义的试错提交面试时这个习惯也会是一个隐形的加分项。6.3 从暴力解法到最优解法的迭代路线面对一道不会的题不要一上来就想着最优解。正确的节奏是先写一个能过的暴力解法计算它的复杂度再思考“瓶颈在哪”最后用更高效的数据结构替换瓶颈部分。前面提到的滑动窗口最大值就是一个例子。暴力解的主循环套内层循环瓶颈在内层的最大值为O(k)引入单调队列后每个元素在队列里最多入队一次、出队一次总操作数就是O(n)。你在面试时可以主动把这个思考过程讲出来这比直接甩出一个最优解更能体现算法功底。7. 复盘与延伸思考栈与队列是线性表的两大特化形式它们的共同特点是“操作受限”——插入删除的位置严格受控。这种限制看似不自由却正好匹配了很多场景的需求。比如表达式求值要求后进先出任务调度要求先进先出一旦你接受了这种“受限”的设定很多题目的解题入口就出现了。再往深处想一想单调栈和单调队列其实分别是栈和队列在“维护最值”场景下的一种强化形式。你掌握了单调队列处理滑动窗口问题之后以后遇到“直方图最大矩形”“接雨水”中的单调栈题目会发现思维线条是一致的。在这几天的算法训练营里我反复和同学们强调一个点刷题不是为了记住答案而是为了积累一种“条件反射”。看到先进后出想到栈看到先进先出想到队列看到滑动窗口里的最值想到单调队列看到括号匹配想到编译原理。一旦这种连接建立起来你的算法水平才会真正跨过“会做题”的坎进入“会解题”的层级。最后说说我自己带训练营时的实操体会。栈与队列这一章最值得你花时间的地方不是把每道题刷一遍而是把“为什么选择这个结构”这个问题彻底想明白。你可以在每天刷题结束后拿一张空白纸把今天做过的每道题都写一行“选这个结构的核心理由”坚持一周你会明显感觉到自己在面对新题时更有方向感。再分享一个小技巧把所有跟栈和队列相关的题统一整理成一个专题库每次做完一题用一句话记录它的“考点标签”。比如“有效的括号”可以打上“栈叫哈希映射”的标签“滑动窗口最大值”可以打上“单调队列下标维护”的标签。等这个专题库积累到30题以上你的检索速度和题目联想能力会远远超过那些只是在刷题数量上堆砌的人。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询