LeetCode-232:用栈实现队列,两个栈一倒腾就是先进先出
本题在线练习:LeetCode 232. 用栈实现队列 — 在线练习(免费 · 无需登录 · AI 辅助)
配套刷题网站 Zero2Leetcode —— 内置本地 OJ + AI 教练,零门槛开刷 Hot 100。
一、题目概述
请你仅使用两个栈实现一个先入先出(FIFO)队列。实现 MyQueue 类,支持以下四个操作:
push(x):将元素 x 推到队列的末尾pop():从队列的开头移除并返回元素peek():返回队列开头的元素empty():如果队列为空,返回true;否则,返回false
说明:你只能使用栈的标准操作——push to top、peek/pop from top、size、is empty。
示例
输入:
["MyQueue", "push", "push", "peek", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 1, 1, false]
解释:
myQueue = MyQueue()
myQueue.push(1) # 队列:[1]
myQueue.push(2) # 队列:[1, 2]
myQueue.peek() # 返回 1(队头元素)
myQueue.pop() # 返回 1(移除队头)
myQueue.empty() # 返回 False
二、核心思路
为什么一个栈做不了队列?
栈是后进先出(LIFO):最后压入的元素最先弹出。
队列是先进先出(FIFO):最先压入的元素最先弹出。
如果只有一个栈,元素 1、2、3 依次压入后,弹出顺序是 3、2、1——和队列要求的 1、2、3 完全相反。想要拿到栈底的元素,必须先把上面的元素全部弹出来,但弹出来又没地方放。
所以一个栈无法实现 FIFO,至少需要两个栈。
两个栈的"翻转"技巧
核心观察:栈会反转元素顺序,那反转两次不就恢复了吗?
我们用两个栈:
in_stack(输入栈):专门负责接收push的元素out_stack(输出栈):专门负责提供pop/peek的元素
规则非常简单:
- push 操作:直接把元素压入
in_stack - pop / peek 操作:
- 如果
out_stack不为空,直接从out_stack取栈顶 - 如果
out_stack为空,把in_stack中的所有元素逐个弹出并压入out_stack,然后再从out_stack取栈顶
- 如果
为什么这样做是正确的?假设依次 push 了 1、2、3:
in_stack从底到顶是:[1, 2, 3](3 在栈顶)- 全部倒入
out_stack后,从底到顶变成:[3, 2, 1](1 在栈顶) - 此时从
out_stack弹出,顺序就是 1、2、3——正是 FIFO!
一句话记忆:"push 进输入栈,pop 时如果输出栈空了就把输入栈整个倒过来。"
三、代码实现
class MyQueue:
def __init__(self):
self.in_stack = []
self.out_stack = []
def push(self, x: int) -> None:
self.in_stack.append(x)
def pop(self) -> int:
self._transfer()
return self.out_stack.pop()
def peek(self) -> int:
self._transfer()
return self.out_stack[-1]
def empty(self) -> bool:
return not self.in_stack and not self.out_stack
def _transfer(self) -> None:
if not self.out_stack:
while self.in_stack:
self.out_stack.append(self.in_stack.pop())
四、逐行拆解
1. 初始化:两个空栈
def __init__(self):
self.in_stack = []
self.out_stack = []
in_stack 是输入栈,所有新元素从这里进入。out_stack 是输出栈,所有要取出的元素从这里出去。两个栈各司其职,互不干扰。
2. push:直接压入输入栈
def push(self, x: int) -> None:
self.in_stack.append(x)
push 操作永远只和 in_stack 打交道,O(1) 时间完成。
3. 核心辅助方法 _transfer:倒腾元素
def _transfer(self) -> None:
if not self.out_stack:
while self.in_stack:
self.out_stack.append(self.in_stack.pop())
这是整个设计最精华的部分。
只有当 out_stack 为空时才会触发倒腾。把 in_stack 里的元素逐个弹出压入 out_stack,这个过程会自然地反转元素顺序。
为什么必须等 out_stack 空了才倒?因为如果 out_stack 还有元素,这些元素是更早 push 的,应该先被 pop。如果此时把 in_stack 的新元素倒进来,就会打乱 FIFO 顺序。
4. pop:取出队头元素
def pop(self) -> int:
self._transfer()
return self.out_stack.pop()
先确保 out_stack 有东西(如果没有就触发倒腾),然后弹出栈顶——这个栈顶就是队列的队头。
5. peek:查看队头但不移除
def peek(self) -> int:
self._transfer()
return self.out_stack[-1]
和 pop 逻辑几乎一样,只是用 [-1] 查看栈顶而不弹出。
6. empty:两个栈都为空才算空
def empty(self) -> bool:
return not self.in_stack and not self.out_stack
队列的元素可能分布在两个栈中,只有两个都为空,整个队列才是空的。
五、手动模拟
以操作序列 push(1), push(2), push(3), peek, pop, push(4), pop, pop, pop 为例,逐步跟踪两个栈的状态。
| 操作 | in_stack | out_stack | 返回值 | 说明 |
|---|---|---|---|---|
| push(1) | [1] | [] | — | 1 压入输入栈 |
| push(2) | [1, 2] | [] | — | 2 压入输入栈 |
| push(3) | [1, 2, 3] | [] | — | 3 压入输入栈 |
| peek() | [] | [3, 2, 1] | 1 | out_stack 空,触发倒腾:弹出 3→2→1 压入 out_stack,栈顶是 1 |
| pop() | [] | [3, 2] | 1 | out_stack 不空,直接弹出栈顶 1 |
| push(4) | [4] | [3, 2] | — | 4 压入输入栈(不影响输出栈) |
| pop() | [4] | [3] | 2 | out_stack 不空,直接弹出栈顶 2 |
| pop() | [4] | [] | 3 | out_stack 不空,直接弹出栈顶 3 |
| pop() | [] | [4] | 4 | out_stack 空了,触发倒腾:弹出 4 压入 out_stack,然后弹出 4 |
弹出顺序:1 → 2 → 3 → 4,完美的 FIFO。
关键观察:push(4) 发生在第二次 pop() 之前,但 4 是最后才被弹出的。因为 out_stack 中还有更早的元素(2 和 3),它们必须先出队。只有当 out_stack 完全清空后,才会把 in_stack 的元素倒过来,4 才有机会出队。
六、复杂度分析
时间复杂度:均摊 O(1)
单次 pop 或 peek 在最坏情况下可能需要 O(n) 时间(把整个 in_stack 倒到 out_stack)。但从均摊(amortized)角度看:
每个元素最多被搬运两次:一次从外部 push 到
in_stack,一次从in_stack倒到out_stack。
所以 n 个元素总共的搬运次数是 2n,平均到每个操作上是 O(1)。
用一个更直观的方式理解:假设你 push 了 100 个元素,第一次 pop 时需要把 100 个元素全部倒过来(O(100)),但接下来的 99 次 pop 都是直接弹出(O(1))。总共 100 次操作花了 200 步,平均每次 2 步,是常数级别。
空间复杂度:O(n)
两个栈总共存储 n 个元素,额外空间是 O(n)。
七、总结
| 要点 | 内容 |
|---|---|
| 核心思想 | 两个栈反转两次 = 恢复原序 = FIFO |
| push | 直接压入 in_stack,O(1) |
| pop / peek | out_stack 非空直接取;为空则把 in_stack 全部倒入 |
| empty | 两个栈都为空才算空 |
| 时间复杂度 | 均摊 O(1)(每个元素最多搬运两次) |
| 空间复杂度 | O(n) |
这道题和 LC225(用队列实现栈)是一对"互逆"问题,放在一起对比很有意思:
| LC232 用栈实现队列 | LC225 用队列实现栈 | |
|---|---|---|
| 核心矛盾 | 栈是 LIFO,队列要 FIFO | 队列是 FIFO,栈要 LIFO |
| 解决手段 | 两个栈,倒腾一次反转顺序 | 一个队列,push 时把前面的元素重新排到后面 |
| push 复杂度 | O(1) | O(n) |
| pop 复杂度 | 均摊 O(1) | O(1) |
两道题的共同启示是:数据结构的"顺序"是可以通过巧妙的操作来改变的。栈的 LIFO 特性看似和 FIFO 水火不容,但只要加一个"中转站"做一次反转,就能完美模拟队列行为。理解这个思路后,遇到"用 A 实现 B"类型的设计题,核心就是找到如何利用 A 的基本操作来模拟 B 的行为特征。

浙公网安备 33010602011771号