一、题目到底在问什么
LeetCode 第 232 题 Implement Queue using Stacks(用栈实现队列),要求你只用两个栈,就实现一个完整的队列。
栈(Stack)和队列(Queue)是两种最常见的线性数据结构,它们的规矩完全不同:
- 栈:后进先出(Last In, First Out)。像一摞盘子,你只能从最上面拿。
- 队列:先进先出(First In, First Out)。像超市排队,先来的人先结账离开。
问题来了:栈只允许"后进先出",而队列要求"先进先出",规则明明是反的,怎么可能实现?答案就藏在"两个栈"里——一个栈负责"进",一个栈负责"出",中间靠一次"倒水"来翻转顺序。
二、核心思路:先倒进去,再倒出来
代码里维护了两个数组(JavaScript 里用数组模拟栈):inStack 和 outStack。分工非常明确:
push:新元素永远先塞进inStack,这是"入口"。pop/peek:从outStack里取,这是"出口"。
关键魔术在 _transfer 这个方法里:
MyQueue.prototype._transfer = function() {
if(this.outStack.length === 0){
while(this.inStack.length > 0){
this.outStack.push(this.inStack.pop());
}
}
};
它做的只有一件事:当 outStack 是空的时,把 inStack 里的元素一个一个弹出来,再依次压进 outStack。
为什么这一步就能把"后进先出"变成"先进先出"?因为栈会把顺序颠倒一次,倒两次就颠倒回来了。
三、用"倒水"理解这个原理
想象 inStack 是一个装满了数字的杯子,数字从上到下依次是 3, 2, 1(3 在最上面)。队列要求先出 1,但 1 在杯底,拿不到。
怎么办?把杯子翻过来倒进另一个杯子 outStack。倒的时候,先进 outStack 的当然是 3,接着是 2,最后是 1。于是 outStack 从上到下就变成了 1, 2, 3——1 跑到最上面了!
这时再 pop,直接从 outStack 顶部拿到 1,完美符合先进先出。整个过程就像把一杯水从左手倒进右手,顺序翻转一次;再从右手倒回左手,顺序又翻转一次,回到最初。
如果你在 inStack 里连续 push 了 1, 2, 3,又连续 pop 了两次,顺序就是 1, 2——和队列一模一样。栈的两个"后进先出"组合在一起,恰好抵消成一个"先进先出"。
四、为什么要判断 outStack 是空的?
_transfer 开头有个 if(this.outStack.length === 0),这个判断是整个代码正确性的关键。
假设 outStack 里已经有 1, 2(1 在顶部),此时你又 push 了 3 进 inStack,然后调用 pop。如果不加判断,就会把 3 也倒进 outStack,变成 1, 2, 3,顶部成了 3,pop 就会错误地返回 3,破坏了顺序。
而加上判断后,只要 outStack 还有货,就先从它取;只有当它倒空了,才把 inStack 里的新货一次性倒过来。这样新旧元素永远不乱序——旧队列先清空,新来的元素永远排在后面。这也保证了时间复杂度的均摊 O(1):每个元素最多被"倒"一次、被取一次,多次操作的均摊代价很小。
五、peek 和 empty 的小细节
peek:只是看一眼队列最前面的元素,但不取走。所以它调用_transfer后,返回outStack的栈顶(即数组最后一个元素),而不像pop那样真的弹出去。empty:队列空不空,要看两个栈都为空。因为元素可能正"倒"在outStack里等着被取,只看inStack会误判。