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

本文主要内容有两个,是如何用双栈 实现队列 和如何用单队列实现 栈。(采用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)。

三、总结

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

相关推荐
kirito学长-Java几秒前
springboot/ssm太原学院商铺管理系统Java代码编写web在线购物商城
java·spring boot·后端
爱学习的白杨树1 分钟前
MyBatis的一级、二级缓存
java·开发语言·spring
OTWOL7 分钟前
两道数组有关的OJ练习题
c语言·开发语言·数据结构·c++·算法
问道飞鱼10 分钟前
【前端知识】强大的js动画组件anime.js
开发语言·前端·javascript·anime.js
拓端研究室11 分钟前
R基于贝叶斯加法回归树BART、MCMC的DLNM分布滞后非线性模型分析母婴PM2.5暴露与出生体重数据及GAM模型对比、关键窗口识别
android·开发语言·kotlin
Code成立12 分钟前
《Java核心技术I》Swing的网格包布局
java·开发语言·swing
Auc2416 分钟前
使用scrapy框架爬取微博热搜榜
开发语言·python
中草药z18 分钟前
【Spring】深入解析 Spring 原理:Bean 的多方面剖析(源码阅读)
java·数据库·spring boot·spring·bean·源码阅读
QQ同步助手23 分钟前
C++ 指针进阶:动态内存与复杂应用
开发语言·c++
信徒_25 分钟前
常用设计模式
java·单例模式·设计模式