双栈模拟队列:从 LIFO 到 FIFO 的转换
在数据结构的学习中,利用一种结构去模拟另一种结构是锻炼算法思维的经典方式。之前我们探讨过如何用队列模拟栈,今天我们来聊聊它的对称问题:如何用两个栈(LIFO)来优雅地模拟队列(FIFO)。
这不仅是理解栈与队列本质的绝佳案例,也是深入掌握摊还时间复杂度分析的实战场景。
设计哲学与架构
核心思想:顺序反转
传统的队列要求元素从一端进入,另一端离开。而栈只能从同一端进出。要用栈模拟队列,关键在于如何将栈中倒置的顺序(后进先出)转化为队列需要的正序(先进先出)。
双栈模型巧妙地将职责拆分给两个独立的栈:
- 输入栈(s1):专门负责入队操作。所有新元素无条件推入这里,像一个高效的'原料仓库'。
- 输出栈(s2):专门负责出队操作。它维护了队列的正确顺序,只在需要提供队头元素时才被使用,像面向客户的'售货柜台'。
数据流向比喻
想象一个餐厅的点菜系统:
- 输入栈 (s1) 是厨房,订单依次堆叠。
- 输出栈 (s2) 是出菜口。
- 转移操作 是配菜:当出菜口空了,厨房将所有订单一次性倒扣到出菜口托盘上。由于栈的反转特性,原本最底下的最早订单变成了出菜口最上面的订单,等待被取出。
这种职责分离确保了 s1 永远只做 O(1) 的入队,而 s2 仅在空时才进行昂贵的转移操作。
核心函数实现解析
入队操作:myQueuePush
// [O(1)] 队列入队操作:将元素 x 压入输入栈 s1
void myQueuePush(MyQueue* obj, int x) {
assert(obj);
// 直接将新元素压入输入栈
STPush(&(obj->s1), x);
}
为什么只需简单压入?因为 s1 的唯一职责就是接收入队请求。新元素 x 压入栈顶后,自然位于当前所有元素之'上'。未来需要出队时,所有元素会被转移到 s2,此时 x 将位于 s2 的栈底,保证了它是最晚出队的元素,完美满足 FIFO 要求。
查看队头:myQueuePeek
这是整个设计的核心,采用了懒惰转移策略。
// [摊还 O(1),最坏 O(N)] 队列查看队头元素操作
int myQueuePeek(MyQueue* obj) {
assert(obj);
// 如果输出栈为空,触发数据转移
if (STEmpty(&(obj->s2))) {
while (!STEmpty(&(obj->s1))) {
STDataType data = STTop(&(obj->s1));
STPop(&(obj->s1));
STPush(&(obj->s2), data);
}
}
return STTop(&(obj->s2));
}


