算法思维升级:从“暴力搬运”到“懒惰思维”,我是如何用双栈搞定队列的?

42 阅读2分钟

算法思维升级:从“暴力搬运”到“懒惰思维”,我是如何用双栈搞定队列的?

前言 最近在刷《剑指 Offer》,遇到了一道非常有意思的题目:用两个栈实现队列。 初看觉得只是数据结构的转换,但细想之下,这里面藏着一个非常重要的工程思维——“懒加载” (Lazy Loading)。 今天复盘一下我的解题心路历程。

1. 问题的本质冲突

我们都知道:

  • 栈 (Stack):是先进后出 (FILO) 的,像一个弹夹。
  • 队列 (Queue):是先进先出 (FIFO) 的,像食堂排队。

要把“弹夹”变成“排队”,我们需要两个杯子来倒腾水。

  • 栈 A (inStack):专门负责进货。
  • 栈 B (outStack):专门负责出货。

2. 第一版思路:勤劳的搬运工

刚开始我的想法很简单粗暴:

  1. 入队时:直接压入 A。
  2. 出队时: 为了拿到 A 最底下的那个元素(最早进来的),我必须把 A 里的东西全部倒进 B 里,弹出最上面那个,然后再把 B 里的东西全部倒回 A(恢复原状)。

缺点显而易见: 每次出队都要搬运两次全部数据,时间复杂度是 O(N)O(N),效率太低。

3. 思维进化:做一个“懒惰”的搬运工

后来我意识到,其实没必要倒回去

当把 A 的元素倒进 B 之后,B 里的元素顺序变成了:栈顶 = 最早入队的元素。 这不正是我们想要的队列顺序吗?

只要 B 里还有东西,我直接从 B 里 pop 出去的就是正确的数据。为什么要倒回去呢?

优化后的策略:

  1. 入队:永远只进 inStack
  2. 出队
    • 先看 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 后端练习生。 如果你也觉得“懒惰”是一种美德,欢迎点赞交流!🚀