【数据结构与算法】之 双栈实现队列 和 单队列实现栈

本文主要内容有两个,是如何用双栈 实现队列 和如何用单队列实现 栈。(采用Java实现)

回忆一下:

栈:

队列:

相关教程:

栈详解

队列详解

栈和队列

一、双栈实现队列

设计目标

设计的目标是使用栈(后进先出 ,LIFO)来实现队列(先进先出,FIFO)的功能。队列的特点是先进入的元素最先被取出,而栈则是最后进入的元素最先被取出。因此,我们需要一种方法使得栈能够表现出队列的行为。

思路分析

1. 使用双栈实现

为了模拟队列的先进先出特性,我们可以使用两个栈:一个作为输入栈 (s1),另一个作为输出栈 (s2)。基本思路如下:

  • s1 作为输入栈,用于接收新元素的入队操作。
  • s2 作为输出栈,用于模拟队列的出队操作。
2. push 操作
  • 思想 :将新元素推入输入栈 s1 中。
  • 实现 :直接将新元素 x 压入 s1 栈中。
3. pop 操作
  • 思想 :从输出栈 s2 中弹出栈顶元素,如果没有元素,则需要将输入栈 s1 中的元素全部转移到输出栈 s2 中。
  • 实现
    • 如果 s2 不为空,则直接从 s2 弹出栈顶元素。
    • 如果 s2 为空,则将 s1 中的所有元素依次弹出并压入 s2,然后从 s2 弹出栈顶元素。
4. peek 操作
  • 思想:查看队列的前端元素但不移除。
  • 实现
    • 如果 s2 不为空,则直接返回 s2 的栈顶元素。
    • 如果 s2 为空,则将 s1 中的所有元素依次弹出并压入 s2,然后返回 s2 的栈顶元素。
5. empty 操作
  • 思想:判断队列是否为空。
  • 实现 :检查 s1s2 是否都为空。

源码:

java 复制代码
class MyQueue {

    private Stack<Integer> s1;// 输入栈
    private Stack<Integer> s2;// 输出栈
    
    public MyQueue() {
        s1 = new Stack<>();
        s2 = new Stack<>();
    }

    public void push(int x) {
        s2.push(x);
    }

    public int pop() {
        if(s1.isEmpty()){
            while(!s2.isEmpty()){
                s1.push(s2.pop());
            }
        }
        return s1.pop();
    }

    public int peek() {
        if (s1.isEmpty()) {
            while (!s2.isEmpty()) {
                s1.push(s2.pop());
            }
        }
        return s1.peek();
    }

    public boolean empty() {
        return s1.isEmpty() && s2.isEmpty();
    }
}

解释:

  • push 方法 :将新元素直接压入 s2
  • pop 方法 :在 s1 为空时,将 s2 中的元素转移到 s1 中,然后从 s1 中弹出栈顶元素。
  • peek 方法 :与 pop 类似,但在 s1 中查看栈顶元素而不移除。
  • empty 方法:检查两个栈是否都为空。
  • 新元素总是被压入 s2
  • 当需要弹出或查看队列前端元素时,如果 s1 为空,则将 s2 中的元素转移到 s1 中。
  • s1 用于模拟队列的前端,而 s2 用于接收新元素并临时存储。
  • push 操作的时间复杂度为 O(1),因为直接将元素压入 s2
  • poppeek 操作的平均时间复杂度为 O(1),因为每个元素只会被转移一次(从 s2s1),并且在后续的操作中可以直接从 s1 中获取。

二、单队列实现栈

设计目标

设计的目标是使用队列(先进先出 ,FIFO)来实现栈(后进先出,LIFO)的功能。栈的特点是最后进入的元素最先被取出,而队列则是先进入的元素先被取出。因此,我们需要一种方法使得队列能够表现出栈的行为。

思路分析

1. 使用单个队列实现

既然栈要求后进先出,那么我们可以把新加入的元素放在队列的末尾,然后通过一系列操作将它移到队列的前端。这样,当我们需要访问栈顶元素时,实际上就是访问队列的前端元素。

2. push 操作
  • 思想 :当新的元素 x 被推入栈时,我们希望 x 成为新的栈顶元素。由于队列是先进先出的,我们需要一种方式使 x 成为队列的前端元素。
  • 实现 :将 x 加入队列的尾部,然后将队列中除了最后一个元素外的所有元素都移到队列的尾部。这样做的结果是,最新的元素 x 将会成为队列的前端元素,符合栈的要求。
3. pop 操作
  • 思想:从栈中弹出栈顶元素,即删除队列的前端元素。
  • 实现:直接从队列的前端移除元素即可。
4. top 操作
  • 思想:获取栈顶元素但不移除。
  • 实现:查看队列前端的元素即可。
5. empty 操作
  • 思想:判断栈是否为空。
  • 实现:检查队列是否为空。
6. 大小管理
  • 思想 :由于 push 操作涉及到元素的重新排列,因此需要一个计数器来记录栈中的元素数量。
  • 实现 :使用一个 size 变量来追踪栈中元素的数量。每次 push 时增加 size,每次 pop 时减少 size

源码:

java 复制代码
class MyStack {

    private Queue<Integer> queue;

    public MyStack() {
        queue = new LinkedList<>();
    }
    
    int size = 0;

    public void push(int x) {
        queue.offer(x);
        for (int i = 0; i < size; i++) {
            queue.offer(queue.poll());
        }
        size++;
    }
    
    public int pop() {
        size--;
        return queue.poll();
    }
    
    public int top() {
        return queue.peek();
    }
    
    public boolean empty() {
        return queue.isEmpty();
    }
}

解释:

  • push 方法:当一个元素被压入栈时,它首先被添加到队列的尾部,然后将队列中除了最后一个元素之外的所有元素都移回队列的末尾。这样做是为了确保新加入的元素实际上处于"顶部",即如果从队列中取出元素,最先取出的是最近压入的元素。

  • pop 方法:弹出栈顶元素,即队列的第一个元素,并返回它。注意在弹出前会先减小 size 变量以保持计数准确。

  • top 方法:返回栈顶元素而不移除它,也就是队列的第一个元素。

  • empty 方法:检查队列是否为空,从而判断栈是否为空。

需要注意的是,在 push 操作中,每次插入新元素都需要进行 size 次队列元素的重新排列。这意味着 push 操作的时间复杂度为 O(n),其中 n 是当前栈中的元素数量。而 pop, top, 和 empty 操作的时间复杂度则为 O(1)。

三、总结

最后,希望通过以上练习可以加深各位对栈和队列这两种数据结构的基本操作有更进一步的掌握和理解,下期见,谢谢~

相关推荐
小安同学iter5 分钟前
Java进阶五 -IO流
java·开发语言·intellij-idea
Hello World and You6 分钟前
R ggplot2 绘图细节 geom_text展示不全 y轴坐标细节 x轴标题
开发语言·r语言
非自律懒癌患者9 分钟前
Transformer中的Self-Attention机制如何自然地适应于目标检测任务
人工智能·算法·目标检测
sukalot10 分钟前
windows C#-异步文件访问
开发语言·c#
尽兴-16 分钟前
Redis模拟延时队列 实现日程提醒
java·redis·java-rocketmq·mq
SSL_lwz22 分钟前
P11290 【MX-S6-T2】「KDOI-11」飞船
c++·学习·算法·动态规划
熬夜学编程的小王29 分钟前
【C++篇】从基础到进阶:全面掌握C++ List容器的使用
开发语言·c++·list·双向链表·迭代器失效
悄悄敲敲敲30 分钟前
C++:智能指针
开发语言·c++
zhangpz_34 分钟前
c ++零基础可视化——vector
c++·算法
书埋不住我36 分钟前
java第三章
java·开发语言·servlet