算法思维升级:从“暴力搬运”到“懒惰思维”,我是如何用双栈搞定队列的?
前言 最近在刷《剑指 Offer》,遇到了一道非常有意思的题目:用两个栈实现队列。 初看觉得只是数据结构的转换,但细想之下,这里面藏着一个非常重要的工程思维——“懒加载” (Lazy Loading)。 今天复盘一下我的解题心路历程。
1. 问题的本质冲突
我们都知道:
- 栈 (Stack):是先进后出 (FILO) 的,像一个弹夹。
- 队列 (Queue):是先进先出 (FIFO) 的,像食堂排队。
要把“弹夹”变成“排队”,我们需要两个杯子来倒腾水。
- 栈 A (
inStack):专门负责进货。 - 栈 B (
outStack):专门负责出货。
2. 第一版思路:勤劳的搬运工
刚开始我的想法很简单粗暴:
- 入队时:直接压入 A。
- 出队时: 为了拿到 A 最底下的那个元素(最早进来的),我必须把 A 里的东西全部倒进 B 里,弹出最上面那个,然后再把 B 里的东西全部倒回 A(恢复原状)。
缺点显而易见: 每次出队都要搬运两次全部数据,时间复杂度是 ,效率太低。
3. 思维进化:做一个“懒惰”的搬运工
后来我意识到,其实没必要倒回去!
当把 A 的元素倒进 B 之后,B 里的元素顺序变成了:栈顶 = 最早入队的元素。 这不正是我们想要的队列顺序吗?
只要 B 里还有东西,我直接从 B 里 pop 出去的就是正确的数据。为什么要倒回去呢?
优化后的策略:
- 入队:永远只进
inStack。 - 出队:
- 先看
outStack有没有货?如果有,直接拿走! (O(1)) - 如果
outStack空了?这才不得已,把inStack里的货一次性全搬过来。 - 如果两个都空了?那就是没数据了,返回 -1。
- 先看
这种“不到万不得已不干活”的思想,不仅节省了算力,也正是我们在做业务开发时常用的“懒加载”思想。
4. 代码实现 (Java)
在 Java 中,官方推荐使用 Deque (双端队列) 接口配合 LinkedList 来替代古老的 Stack 类。
class CQueue {
// 两个栈:一个只管进,一个只管出
Deque<Integer> inStack;
Deque<Integer> outStack;
public CQueue() {
inStack = new LinkedList<>();
outStack = new LinkedList<>();
}
// 入队:O(1)
public void appendTail(int value) {
inStack.push(value);
}
// 出队:摊还复杂度 O(1)
public int deleteHead() {
// 1. 如果出货栈不为空,直接出货,这是最快的情况
if (!outStack.isEmpty()) {
return outStack.pop();
}
// 2. 如果出货栈空了,看看进货栈有没有数据
if (inStack.isEmpty()) {
return -1; // 两个都空,没货
}
// 3. 【关键】懒惰搬运:一次性把进货栈的数据全搬过来
while (!inStack.isEmpty()) {
outStack.push(inStack.pop());
}
// 4. 搬完后,出货
return outStack.pop();
}
}
我是 IT_星Star,叫我星星,一个正在死磕算法、冲刺大厂的 Java 后端练习生。 如果你也觉得“懒惰”是一种美德,欢迎点赞交流!🚀