232. 用栈实现队列(Implement Queue using Stacks)
频次 ★★★ · 难度 🟢 · 高频:全厂
题目
仅使用两个栈实现一个先进先出(FIFO)队列,支持 push、pop、peek、empty。
思路
栈是后进先出,队列是先进先出,正好相反。用两个栈倒腾一次顺序就能互相转换:
inStack只负责接收push进来的元素。outStack负责pop/peek:当它为空时,把inStack里的元素全部弹出再压入outStack——这个过程会把顺序整体反转一次,最早push的元素就跑到了outStack的栈顶,也就是队首。- 只要
outStack不为空,就一直从它出队,不需要重新倒腾(摊还下来每个元素只被”倒腾”一次)。
代码
class MyQueue {
private final Deque<Integer> inStack = new ArrayDeque<>();
private final Deque<Integer> outStack = new ArrayDeque<>();
public void push(int x) {
inStack.push(x);
}
public int pop() {
moveIfNeeded();
return outStack.pop();
}
public int peek() {
moveIfNeeded();
return outStack.peek();
}
public boolean empty() {
return inStack.isEmpty() && outStack.isEmpty();
}
private void moveIfNeeded() {
if (outStack.isEmpty()) {
while (!inStack.isEmpty()) {
outStack.push(inStack.pop());
}
}
}
}复杂度
- 时间:
pushO(1);pop/peek最坏 O(n),但均摊 O(1)——每个元素一生只会从inStack倒到outStack一次 - 空间:O(n) — 两个栈总共存 n 个元素
边界条件
- 空队列调用
pop/peek:题目通常保证调用前队列非空,moveIfNeeded()在两栈都为空时不会出错(while循环不执行),但outStack.pop()/peek()会在真正空的情况下抛异常,工程实现中可按需要额外判空。 - 连续多次
push后只pop一次:只有第一次pop(此时outStack为空)才会触发倒腾,之后的pop直接从outStack取,不会重复倒腾。 - 交替
push/pop:每次outStack为空时才倒腾,均摊下来每个元素只被倒腾一次。
变式
- 反过来”用队列实现栈”是 225. 用队列实现栈,思路不对称:栈转队列靠”倒腾一次换方向”,队列转栈靠”每次 push 后手动把新元素转到队首”。
- 只用一个栈 + 递归实现队列:
push时先递归弹出所有元素,压入新元素后再依次放回——本质上是用递归调用栈模拟第二个栈,思路相通但代码更绕。
易错点
moveIfNeeded()的判断条件是outStack.isEmpty(),不是inStack.isEmpty()——只有当outStack空了才需要倒腾,如果outStack还有剩余元素就不能倒腾(会打乱已经调整好的顺序)。- 倒腾操作必须一次性把
inStack全部倒完,不能只倒一部分,否则两个栈中元素的相对顺序会被打乱。
面试追问
- 均摊 O(1) 具体是怎么算出来的? 每个元素从
push进inStack到最终被pop出去,最多经历”一次压入inStack、一次弹出inStack压入outStack、一次从outStack弹出”,一共 3 次基本操作;n 个元素总共最多 3n 次操作,均摊到每次pop调用就是 O(1)。 - 能不能只用一个栈实现队列? 不能用一个栈简单实现(栈本身的后进先出特性和队列的先进先出是反的),至少需要两个栈(或一个栈+递归调用栈)来完成一次”顺序反转”,才能把后进先出转换成先进先出。
关联题
- 同套路:225. 用队列实现栈 —— 互为反向问题;155. 最小栈 —— 同属结构设计
- 知识点:倒栈摊还 O(1)——每个元素一生最多进出两次;同一分析方法用在 ArrayList 扩容上,见集合框架