双栈实现队列以及双队列实现栈

栈是 后进先出(LIFO),队列是 先进先出(FIFO)

一、双栈实现队列

核心思路:

实现的关键是给两个栈定好角色:

  • 入队栈:只负责 "接收新元素"(入队操作直接压栈);
  • 出队栈:只负责 "输出元素"(出队 / 取队首时从这里拿)。

核心操作逻辑就 3 步:

  1. 入队(offer) :直接把元素压入inStack(简单粗暴,O (1));
  2. 出队 / 取队首(poll/peek)
    • outStack为空,把inStack里的所有元素弹出并压入 outStack(这一步会反转元素顺序,把栈的 LIFO 变成队列的 FIFO);
    • outStack非空,直接从outStack弹出 / 查看栈顶(就是队列的队首);
  3. 判空(isEmpty):两个栈都为空时,队列才是空的。

代码实现:

java 复制代码
/**
 * 双栈实现队列
 */
public class MyQueue {
    // 入队栈
    private final Deque<Integer> inStack;
    // 出队栈
    private final Deque<Integer> outStack;

    public MyQueue() {
        // 用LinkedList实现Deque
        inStack = new LinkedList<>();
        outStack = new LinkedList<>();
    }
    /**
     * 入队操作
     */
    public void offer(int x) {
        inStack.push(x);
    }
    /**
     * 出队操作
     */
    public int poll() {
        // 确保出队栈有元素
        transferInToOut();
        if (outStack.isEmpty()) {
            throw new NoSuchElementException("队列已空,无法出队");
        }
        return outStack.pop();
    }
    /**
     * 获取队首元素(不移除)
     * 队列为空时抛出NoSuchElementException
     */
    public int peek() {
        transferInToOut();
        if (outStack.isEmpty()) {
            throw new NoSuchElementException("队列已空,无法获取队首");
        }
        return outStack.peek();
    }

    /**
     * 判断队列是否为空
     */
    public boolean isEmpty() {
        return inStack.isEmpty() && outStack.isEmpty();
    }

    /**
     * 仅当出队栈为空时,将入队栈所有元素转移到出队栈
     */
    private void transferInToOut() {
        if (outStack.isEmpty()) {
            while (!inStack.isEmpty()) {
                outStack.push(inStack.pop());
            }
        }
    }

}

细节:

1、元素转移的时机:仅当 outStack 为空时转移

2、peek 方法别丢数据:peek 是 "查看队首" 不是 "拿走",所以不能直接 pop 元素 ------ 代码里通过outStack.peek()获取即可,不需要修改栈结构。

3、别用错栈的实现类:别用ArrayList实现DequeArrayListremove(0)(对应栈的pop)是 O (n) 复杂度,而LinkedListpop是 O (1),效率差很多。

4、空队列要抛异常,别返回默认值:空队列执行 poll/peek 时,返回 0 会让用户误以为 "队首是 0"------ 正确做法是抛NoSuchElementException,和 Java 原生Queue(比如LinkedList)的行为保持一致。

时间复杂度

  • 入队(offer):O (1)------ 直接压入 inStack,无额外操作;
  • 出队(poll)/ 取队首(peek)均摊 O (1)------ 每个元素只会从 inStack 转移到 outStack 一次,后续出队直接从 outStack 拿,整体平均下来是 O (1)。

二、双队列实现栈

核心思路:

始终保持一个队列为空,另一个队列存所有元素,通过 "转移元素" 实现栈的 LIFO:

  1. 入栈(push) :直接把元素加到非空队列 里(如果都空,随便选一个,比如q1);
  2. 出栈(pop) :把非空队列里的前 n-1 个元素移到空队列,剩下的最后一个元素就是栈顶,直接弹出;
  3. 取栈顶(top):逻辑和 pop 一样,但弹出后要把这个元素再放到空队列里(别丢数据!);
  4. 判空(empty):两个队列都为空时,栈才是空的。

代码实现:

java 复制代码
public class MyStack {
    //栈:先进后出
    //队列:先进先出
    /**
     * 双队列实现栈
     */
    //定义两个队列
    private Queue<Integer> q1;
    private Queue<Integer> q2;
    /**
     *初始化两个空队列
     */
        public MyStack(){
        q1 = new LinkedList<>();
        q2 = new LinkedList<>();
    }
    /**
     * 入栈操作:将元素加入非空队列
     * @param x
     */
    public void push(int x){
        // 优先加入非空队列
        if (!q1.isEmpty()){
            q1.offer(x);
        }else {
            q2.offer(x);
        }
    }

    /**
     * 出栈操作:弹出栈顶元素
     * @return
     */
    public int pop(){
        Queue<Integer> src = q1.isEmpty() ?q2:q1;
        Queue<Integer> dst = q1.isEmpty() ?q1:q2;
        //将源队列前n-1个元素放到目标队列
        while (src.size()>1){
            dst.offer(src.poll());
        }
        //弹出最后一个元素
        return src.poll();
    }

    /**
     * 获取栈顶元素
     * @return
     */
    public int top(){
        Queue<Integer> src = q1.isEmpty() ? q2 : q1;
        Queue<Integer> dst = q1.isEmpty() ? q1 : q2;

        // 移动前n-1个元素到目标队列
        while (src.size() > 1) {
            dst.offer(src.poll());
        }

        // 记录栈顶元素,并将其移到目标队列
        int top = src.poll();
        dst.offer(top);
        return top;
    }
    /**
     * 判断栈是否为空
     * @return 栈空返回true,否则返回false
     */
    public boolean empty() {
        return q1.isEmpty() && q2.isEmpty();
    }
    
}

细节:

1、用**final** 修饰q1q2,必须在构造器里初始化------ 否则 IDE 会提示 "final 变量未初始化"

2、top 操作只是 "看一眼栈顶",不是 "拿走"。所以弹出 src 的最后一个元素后,必须把它加到 dst 里,否则这个元素就丢了

3、别用ArrayList实现 Queue!因为ArrayListremove(0)(对应队列的poll())是 O (n) 复杂度,而LinkedListpoll()是 O (1),效率高多了。

4、空栈执行 pop/top 时,直接返回 0 会误导用户 ------ 应该抛NoSuchElementException,和 Java 原生栈(Stack类)的行为保持一致。

时间复杂度:

  • push(入栈):O (1)------ 直接加元素到队列尾部,不用动其他元素;
  • pop/top(出栈 / 取栈顶):O (n)------ 需要把 n-1 个元素从一个队列移到另一个队列。

用双队列实现栈的核心,其实是用 "队列的元素转移" 模拟 "栈的后进先出"------ 始终保持一个队列为空,通过移动元素让 "最后入队的元素" 变成 "队列的头元素"。

相关推荐
Seven972 小时前
回溯算法总结
java
小鸡脚来咯2 小时前
软链接的作用和用途
java·ide·eclipse
Bruce_kaizy2 小时前
c++图论——最短路之Johnson算法
开发语言·数据结构·c++·算法·图论
“抚琴”的人2 小时前
C#上位机观察者模式
开发语言·观察者模式·c#·上位机
思成Codes2 小时前
Go语言的多返回值是如何实现的?
开发语言·后端·golang
廋到被风吹走2 小时前
【Spring】Spring Batch 详细介绍
java·spring·batch
北极糊的狐2 小时前
MQTT报错:Exception in thread main java.lang.at io.github.pnoker.common.sdk.utils.ParseUtils.decodeHex
java·开发语言
Grassto2 小时前
Go 是如何解析 `import path` 的?第三方包定位原理
开发语言·golang·go module·go import
福大大架构师每日一题2 小时前
go-zero v1.9.4 版本发布详解:云原生适配升级与稳定性性能全面提升
开发语言·云原生·golang