LeetCode 225这道题,几乎所有刷到“栈和队列”专题的人都会路过。表面上看,它只是让你用队列实现一个栈,但真的动手写的时候,很多人会卡在同一个地方:队列是先进先出,栈是后进先出,这两种容器的出队方向天生拧着,怎么才能用队列把一个“后进来的先出去”的行为完整复刻出来?这篇是我自己从第一次看官方题解一头雾水,到能不看答案写出三种解法的完整记录,适合正在刷 LeetCode 栈与队列专题的人,也适合准备面试前想把手底数据结构真正理清的同学。我会把这题的几个核心思路、带注释代码、容易踩的坑,还有面试官常追问的内容一次聊透。
1. 题目到底在考什么:先理清栈和队列的顺序矛盾
1.1 题干逐句翻译
原题要求的接口很简单:实现一个类 MyStack,里面有 push(int x)、pop()、top()、empty() 四个方法,pop 和 top 都要求返回栈顶元素。麻烦的是题目加了一句限制:你只能使用队列的标准操作,也就是从队尾入队、从队首出队、查看队首、获取队列大小、判断队列是否为空。
换句话说,你不能直接拿一个数组倒过来用,也不能用双端队列的 addFirst、removeLast 这种作弊操作。你能动的只有队首和队尾这两个口,而且数据只允许从队尾进来、从队首出去。这个限制一加上,问题从“实现一个类”变成了“用一类线性容器去模拟另一类线性容器的行为”。
这也引出了计算机里一个很重要的抽象思维:只要接口行为对外一致,内部用什么东西实现并不重要。调用方不关心你的栈底层是数组、链表还是队列,他们只关心 push 进去的元素,pop 的时候能不能按照后进先出的顺序出来。
1.2 表格化对比:栈和队列的“下一个出口”
要搞懂解法,先记住一个基本矛盾:栈的出口在“最后进来的元素”那里,队列的出口固定在“最先进来的元素”那里。
| 容器 | 入容器顺序 | 出容器顺序 | 下一个被弹出的元素 |
|---|---|---|---|
| 栈 | 1, 2, 3 | 3, 2, 1 | 最后进入的 3 |
| 队列 | 1, 2, 3 | 1, 2, 3 | 最先进入的 1 |
| 本题目标 | push(1), push(2), push(3) | 3, 2, 1 | 最后进入的 3 |
如果你 push 了 1、2、3,普通队列的队首是 1,队尾是 3。但栈要求 pop 先拿到 3。这中间差了一整个队列的距离,所有解法本质上都是在想方设法让“最新进来的元素”能够出现在队首,或者说让队首的含义等于栈顶。
基于这个矛盾,解题路径可以分成两条:一条是在 push 的时候就把顺序处理好,让新元素永远站在队首;另一条是 push 先不管,等 pop 或 top 的时候再去队尾把最后一个元素翻出来。两条路都能走通,但代码逻辑和复杂度分布差别很大。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 解法一:push 时就让新元素站到队首,pop 只用 O(1)
2.1 两队列实现:临时队列当“排队助手”
第一种思路很好理解。既然栈顶是最后进来的元素,那我在 push 的时候就直接把队列内部顺序反转过来,让队首永远等于栈顶。具体做法是准备两个队列,一个主队列 q1 存放调整好的元素,另一个辅助队列 q2 作为临时中转区。
每次 push 一个数字 x 时,先把 x 放到空的 q2 里,然后把 q1 里已有的所有元素依次搬到 q2。操作完成后,x 反而变成了 q2 的队首,而原来的老元素都排在它后面。最后交换 q1 和 q2,让主队列继续持有全部数据。
这里有个关键点:为什么 x 先入队,老元素后入队,x 反而成了队首?因为队列的特性是“先入先出”,x 先进去,所以它在最前面。老元素是被追加到 x 后方的。从直观上说,x 后到却插队到了最前面,正好对应了“后进先出”的需求。
2.2 两个队列变成转圈的优化:一个队列就够了
如果认真观察上一节的搬移过程,会发现其实没必要开两个队列。辅助队列 q2 的作用只是“提供一个空位置,让新元素插进来”,但队列里的元素本来就可以从队首取出后重新放到队尾,这相当于让队列自己转圈。
单队列的 push 思路是:先把 x 正常入队,放在队尾,然后从队首开始,把前面的元素一个一个拿出来塞回队尾。需要搬多少个?如果队列现在总共有 n 个元素,x 进来之后排在最后,为了
