当前:LC232 · 用栈实现队列 · 首次出现于 Day 12 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

LC232 · Implement Queue using Stacks · 设计

用栈实现队列:倒一次,队头就在顶上

栈只能从同一头进出。入队先堆进 in;要出队时把 in 整摞倒进 out,最早的那个翻到顶上,再弹就符合先进先出。

push 只压 in。pop 或 peek 时若 out 为空,把 in 全部弹出入 out,in 底部最早的元素成为 out 顶部。out 非空就绝不再倒。每个元素最多被搬一次,均摊 O(1),最坏单次 O(n),空间 O(n)。

时间 均摊 O(1)空间 O(n)结论先行 · 全文约 6 节
导读

只能用栈的压入、弹出、看栈顶,实现队列的入队、出队、看队头、判空。栈是后进先出:最后放进去的最先出来。队列是先进先出:先放进去的先出来。两边开口习惯相反。

主例依次 push 1、2、3,再 pop。队列应当弹出 1,留下 2、3。若只用一只栈,弹出的会是 3,顺序反了。

本文要回答:为什么倒一次就能把顺序正过来,为什么 out 里还有货时不能再倒,以及主例三次入栈、一次倒栈、一次弹出分别把两只栈变成什么样。

in 只收新的,out 只发旧的

第一直觉是每次入队都把旧栈倒进临时栈、放下新元素、再倒回来,让栈底永远是队头。这样做对,但每一次 push 都是线性搬运,n 次入队就是平方级。队列真正频繁的往往是「先堆一串,再取一串」,不该为每一次入队付清全部搬家费。

缺的是把「反转」推迟到真正需要队头的那一刻。一只栈负责收新元素,叫 in;另一只栈负责发旧元素,叫 out。push 只往 in 上压,完全不管出队顺序。等到 pop 或 peek,如果 out 是空的,再把 in 里的东西一个一个弹出来、压进 out。倒一次,顺序反一次:原先压在 in 底的最早元素,现在正好在 out 顶。

为什么倒两次等于没倒?元素进 in 时已经被反过一次(后进的在顶上);整摞倒进 out,再反一次,最早的回到最上面。此后 out 顶就是队头,弹出即可。out 里只要还剩东西,就说明队头仍在 out 顶,新来的 push 继续进 in,两伙人互不打扰。

用手走主例。push 1、2、3,in 从底到顶是 1、2、3,out 空,演示第一帧。第一次 pop 时 out 空,开始倒:3 先从 in 弹进 out,接着 2,最后 1。in 空了,out 从底到顶是 3、2、1,顶上是 1,这是演示第二帧。

弹出 1 之后,out 剩下 3、2,顶上是 2,这是第三帧。下一次 pop 不必再倒,直接得 2。先进的 1 先出,FIFO 成立。

均摊为什么是常数:每个元素只进 in 一次、进 out 一次、出 out 一次。把 n 次操作的搬家总次数摊回去,平均每次是常数。某一次 pop 可能倒掉整摞 in,最坏是 O(n),但这些元素以后再 pop 就是 O(1)。empty 看两只栈是否都空;peek 和 pop 一样先保证 out 非空,再读栈顶,peek 不弹。

out 非空时若强行再倒,新倒进去的会压在旧队头上面,顺序立刻乱。主例若在弹出 1 之后立刻 push 4,4 应待在 in 里;此时再 pop,必须先拿 out 顶的 2,而不是去碰 4。这是这道设计题最容易写错的一句。

均摊分析倒栈看起来像 O(n),但每个元素一生只被倒一次。n 次 push/pop 加起来,搬运总次数仍是 O(n),所以均摊每次 O(1)。不要在 out 非空时倒第二次,那不是优化,是把已经排好的队头埋住。
push 1,2,3 → pop
in 栈
123
out 栈
push 1,2,3
三个元素依次入 in 栈

Go:双栈

solution.goGo
type MyQueue struct { in, out []int }
func Constructor() MyQueue { return MyQueue{} }
func (q *MyQueue) Push(x int) {
q.in = append(q.in, x)
}
func (q *MyQueue) transfer() {
if len(q.out) > 0 { return }
for len(q.in) > 0 {
n := q.in[len(q.in)-1]
q.in = q.in[:len(q.in)-1]
q.out = append(q.out, n)
}
}
func (q *MyQueue) Pop() int {
q.transfer()
n := q.out[len(q.out)-1]
q.out = q.out[:len(q.out)-1]
return n
}

1Push 只碰 in,绝不先倒栈。切片末尾当栈顶。

2transfer 开头那句 if len(q.out) > 0 是不变量:out 还有人,队头就在 out 顶,不能把 in 压上去。

3主例三次 Push 后 in=[1,2,3];第一次 Pop 触发倒栈,out=[3,2,1],弹出 1。之后 out 非空,再 Pop 直接得 2。

总结

in 收新的;out 空才整摞倒过去。倒一次,最早的翻到顶。主例先弹出 1。

  • 一只栈收、一只栈发。两次后进先出叠在一起,变成先进先出。
  • 主例 in 里 1、2、3 倒进 out 后顶上是 1;弹出 1 以后顶上是 2,不必再倒。
  • out 非空时再倒会把队头埋住。每个元素只搬一次,均摊常数。
同族题目
LC225用队列实现栈LC155最小栈LC20有效的括号