LeetCode-232:用栈实现队列,两个栈一倒腾就是先进先出

本题在线练习LeetCode 232. 用栈实现队列 — 在线练习(免费 · 无需登录 · AI 辅助)

配套刷题网站 Zero2Leetcode —— 内置本地 OJ + AI 教练,零门槛开刷 Hot 100。

一、题目概述

请你仅使用两个栈实现一个先入先出(FIFO)队列。实现 MyQueue 类,支持以下四个操作:

  • push(x):将元素 x 推到队列的末尾
  • pop():从队列的开头移除并返回元素
  • peek():返回队列开头的元素
  • empty():如果队列为空,返回 true;否则,返回 false

说明:你只能使用栈的标准操作——push to toppeek/pop from topsizeis 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 的元素

规则非常简单:

  1. push 操作:直接把元素压入 in_stack
  2. 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)

单次 poppeek 在最坏情况下可能需要 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 的行为特征。

posted @ 2026-04-20 00:21  孤飞  阅读(41)  评论(0)    收藏  举报