栈和队列

写在前面

栈和队列,也属于线性表,因为它们也都用于存储逻辑关系为 “一对一” 的数据。使用栈结构存储数据,讲究 先进后出,即最先进栈的数据,最后出栈;使用队列存储数据,讲究 先进先出,即最先进队列的数据,也最先出队列。

什么是栈

栈是一种只能从表的一端存取数据且遵循 “先进后出” 原则的线性存储结构,同顺序表链表一样,也是用来存储逻辑关系为 “一对一” 数据。栈的应用有很多,比如浏览器的跳转和回退机制等。

栈和队列

从上图可以看出:

  1. 栈只能从一端存取数据,而另外一端是封闭的
  2. 无论是存还是取,都需要遵循 先进后出的原则。如需要取元素1,需要提前将元素4,3,2取出。

通常,栈的开口端被称为栈顶;封口端被称为栈底。

栈和队列

进栈和出栈

对于栈的操作一般是如下两种:

  1. 进栈:将新元素放到 栈顶元素的上面,成为新的栈顶元素(入栈或压栈);
  2. 出栈:将 栈顶元素删除掉,使得与其相邻的元素成为新的栈顶元素(退栈或弹栈);

栈和队列

栈的存储结构

  • 顺序结构:使用数组实现
  • 链式结构:使用链表实现

顺序栈

栈和队列

一个简单的队列,包含以下方法:

方法 说明 push(E e) 入栈 pop() 出栈 peek() 获取栈头部元素 isEmpty() 判断栈是否为空

代码测试

public class Stack {

    /**
     * 栈的大小
     */
    private int maxSize;
    /**
     * 数组
     */
    private int[] stackArray;
    /**
     * 栈的top
     */
    private int top;

    /**
     * 初始化栈
     *
     * @param size
     */
    public Stack(int size) {
        this.maxSize = size;
        stackArray = new int[maxSize];
        top = -1;
    }

    /**
     * 入栈
     *
     * @param i
     */
    public void push(int i) {
        stackArray[++top] = i;
    }

    /**
     * 常量出栈
     *
     * @return
     */
    public int pop() {
        return stackArray[top--];
    }

    /**
     * 判空
     *
     * @return
     */
    public boolean isEmpty() {
        return (top == -1);
    }

    /**
     * 是否已满
     *
     * @return
     */
    public boolean isFull() {
        return (top == maxSize);
    }

    @Override
    public String toString() {
        for (int i : stackArray) {
            System.out.println("top:"+i);
        }
        return super.toString();
    }

    //测试
    public static void main(String[] args) {
        Stack stack = new Stack(5);
        //入栈
        stack.push(1);
        stack.push(2);
        stack.push(3);
        stack.push(4);
        stack.push(5);
        while (!stack.isEmpty()){
            //出栈
            int pop = stack.pop();
            System.out.println("出栈元素:"+pop);
        }
    }
}

测试结果可以发现显示的数据和输入的数据恰好相反。这也就符合 “先进后出”的原则。

栈和队列

实际运用:字符反转

public class StackReverse {

    /**
     * 栈的大小
     */
    private int maxSize;
    /**
     * 数组
     */
    private char[] stackArray;
    /**
     * 栈的top
     */
    private int top;

    public StackReverse() {
    }

    /**
     * 初始化栈
     *
     * @param size
     */
    public StackReverse(int size) {
        this.maxSize = size;
        stackArray = new char[maxSize];
        top = -1;
    }

    /**
     * 入栈
     *
     * @param c
     */
    public void push(char c) {
        stackArray[++top] = c;
    }

    /**
     * 常量出栈
     *
     * @return
     */
    public char pop() {
        return stackArray[top--];
    }

    /**
     * 判空
     *
     * @return
     */
    public boolean isEmpty() {
        return (top == -1);
    }

    /**
     * 是否已满
     *
     * @return
     */
    public boolean isFull() {
        return (top == maxSize);
    }

    public String doReverse(String str) {
        System.out.println("输入字符为:" + str);
        String rev = "";
        int stackSize = str.length();
        StackReverse stack = new StackReverse(stackSize);
        for (int i = 0; i < stackSize; i++) {
            //&#x83B7;&#x53D6;&#x5B57;&#x7B26;
            char c = str.charAt(i);
            //&#x5165;&#x6808;
            stack.push(c);
        }
        while (!stack.isEmpty()) {
            char pop = stack.pop();
            rev = rev + pop;
        }

        return rev;
    }

    public static void main(String[] args) {
        String rev = new StackReverse().doReverse("reverse");
        System.out.println("&#x5B57;&#x7B26;&#x53CD;&#x8F6C;&#x540E;&#xFF1A;" + rev);
    }
}

测试结果:

栈和队列

之前我面试碰到过,我当时是这样回答的:直接调用reverse方法就行了啊。现在想想无地自容啊,因为面试官问的是实现方式,当然还有很多方法去实现,比如递归等等,这里只是举个例子。

链栈

栈和队列
public class StackByLink<e> {

    private Node<e> top;

    /**
     * &#x8FD4;&#x56DE;&#x6808;&#x9876;&#x5143;&#x7D20;
     *
     * @return
     */
    public E peek() {
        return top.getData();
    }

    /**
     * &#x51FA;&#x6808;
     *
     * @return
     */
    public E pop() {
        E data = null;
        if (top != null) {
            data = top.getData();
            //&#x628A;&#x6808;&#x9876;&#x5143;&#x7D20;&#x5F39;&#x51FA;&#xFF0C;&#x5E76;&#x628A;&#x539F;&#x6808;&#x9876;&#x7684;&#x4E0B;&#x4E00;&#x4E2A;&#x5143;&#x7D20;&#x8BBE;&#x4E3A;&#x6808;&#x9876;&#x5143;&#x7D20;
            top = top.getNext();
        }
        return data;
    }

    /**
     * &#x5165;&#x6808;
     * &#x4E0D;&#x592A;&#x6E05;&#x695A;&#x7684;&#x53EF;&#x4EE5;&#x770B;&#x4E0A;&#x9762;&#x7684;&#x56FE;
     * &#x628A;an&#x8BBE;&#x4E3A;&#x65B0;&#x6808;&#x9876;&#x5143;&#x7D20;node  an-1&#x4E3A;&#x539F;&#x6765;&#x7684;&#x6808;&#x9876;&#x5143;&#x7D20; an-1&#x7684;&#x524D;&#x4E00;&#x4E2A;&#x5143;&#x7D20;&#x5C31;&#x662F;an&#x5373;node
     *
     * @param data
     */
    public void push(E data) {
        Node<e> node = new Node<e>();
        node.setData(data);
        if (top == null) {
            top = node;
        } else {
            //&#x539F;&#x6765;&#x7684;&#x5143;&#x7D20;&#x8BBE;&#x4E3A;&#x65B0;&#x6808;&#x9876;&#x7684;&#x4E0B;&#x4E00;&#x4E2A;&#x5143;&#x7D20;
            node.setNext(top);
            //
            top.setPre(node);
            top = node;
        }
    }

    /**
     * &#x5224;&#x7A7A;
     *
     * @return
     */
    public boolean isEmpty() {
        return (top == null);
    }

    class Node<e> {
        E data;
        //&#x524D;&#x9A71;&#x8282;&#x70B9;
        Node pre;
        //&#x540E;&#x7EE7;&#x8282;&#x70B9;
        Node next;
        ...&#x7701;&#x7565;get set
    }

    //&#x6D4B;&#x8BD5;
    public static void main(String[] args) {
        StackByLink<string> stack = new StackByLink<>();
        stack.push("&#x6211;");
        stack.push("&#x662F;");
        stack.push("&#x94FE;");
        stack.push("&#x8868;");
        System.out.println("peek:" + stack.peek());
        stack.push("&#x5B9E;");
        stack.push("&#x73B0;");
        stack.push("&#x7684;");
        stack.push("&#x6808;");
        System.out.println("pop:" + stack.pop());
        System.out.println("peek:" + stack.peek());
    }
}
</string></e></e></e></e></e>

Java中的栈

java中的栈是通过继承Vector来实现的,如下:

栈和队列

Vector作为List的另外一个典型实现类,支持List的全部功能,Vector类也封装了一个动态的,允许在分配的Object[]数组,Vector是一个比较古老的集合,JDK1.0就已经存在,建议尽量不要使用这个集合,Vector与ArrayList的主要是区别是,Vector是线程安全的,但是性能比ArrayList要低,主要原因是每个方法都是通过synchronized来保证线程同步的。

所以问题就来了,既然只是为了实现栈,为什么不用链表来单独实现,只是为了复用简单的方法而迫使它继承Vector(Stack和Vector本来是毫无关系的)。这使得Stack在基于数组实现上效率受影响,另外因为继承Vector类,Stack可以复用Vector大量方法,这使得Stack在设计上不严谨,当我们看到Vector中:

public void add(int index, E element) {
    insertElementAt(element, index);
}

该方法可以在指定位置添加元素,这与Stack的设计理念相冲突(栈只能在栈顶添加或删除元素),我估计当时的开发者肯定是偷懒了。

什么是队列

队列是一种要求数据只能从一端进,从另一端出且遵循 “先进先出” 原则的线性存储结构

通常,称进数据的一端为 “队尾”,出数据的一端为 “队头”,数据元素进队列的过程称为 “入队”,出队列的过程称为 “出队”。生活中常见的排队买票和医院挂号都是队列。

栈和队列

队列的存储结构

  • 顺序结构:使用数组实现
  • 链式结构:使用链表实现

一个简单的队列,包含以下方法:

方法 说明 add(E e) 入队 remove() 删除此队列的头部 peek() 获取队列头部元素 isEmpty() 判断队列是否为空

顺序队列

public class Queue {

    private int maxSize;
    private int[] queueArray;
    private int front;
    private int rear;
    private int count;

    /**
     * &#x521D;&#x59CB;&#x5316;
     * @param s
     */
    public Queue(int s) {
        this.maxSize = s;
        queueArray = new int[s];
        front = 0;
        rear = -1;
        count = 0;
    }

    /**
     * &#x5165;&#x961F;
     * @param i
     */
    public void add(int i) {
        if (rear == maxSize - 1) {
            rear = -1;
        }
        queueArray[++rear] = i;
        count++;
    }

    /**
     * &#x51FA;&#x961F;
     * @return
     */
    public int remove() {
        int temp = queueArray[front++];
        if (front == maxSize) {
            front = 0;
        }
        count--;
        return temp;
    }

    /**
     * &#x83B7;&#x53D6;&#x961F;&#x5217;&#x5934;&#x90E8;&#x5143;&#x7D20;
     * @return
     */
    public int peek() {
        return queueArray[front];
    }

    public boolean isEmpty() {
        return (count == 0);
    }

    @Override
    public String toString() {
        return "&#x5165;&#x961F;&#x5143;&#x7D20;&#xFF1A;" + Arrays.toString(queueArray);
    }

    public static void main(String[] args) {
        Queue queue = new Queue(5);
        queue.add(1);
        queue.add(2);
        queue.add(3);
        queue.add(4);
        queue.add(5);
        System.out.println(queue);
        while (!queue.isEmpty()){
            int remove = queue.remove();
            System.out.println("&#x51FA;&#x961F;&#x5143;&#x7D20;&#xFF1A;"+remove);
        }
    }
}

测试结果

栈和队列

链式队列

队列的链式存储结构,本质就是线性表的单链表,但它只能 尾进头出,可参考前面的单链表,这里不再复述。

栈和队列

注:以下队列只做介绍,并附上详细讲解连接,感兴趣的可以自行查看

双端队列

栈和队列

双端队列Deque是一种具有 队列和栈的性质的数据结构。双端队列中的元素可以从两端弹出,其限定插入和删除操作在表的两端进行。

ArrayDequeDeque接口的一个实现,使用了可变数组,所以没有容量上的限制。 同时, ArrayDeque是线程不安全的,在没有外部同步的情况下,不能再多线程环境下使用。

ArrayDequeDeque的实现类,可以作为栈来使用,效率高于 Stack

也可以作为队列来使用,效率高于 LinkedList

需要注意的是, ArrayDeque不支持 null值。

可参考:ArrayDeque类的使用详解

阻塞队列

当队列是空的时候,从队列 获取元素的操作将会被阻塞,当队列是满的时候,从队列 插入元素的操作将会被阻塞。后面会在并发编程的JUC中讲到。

Java中阻塞队列BlockingQueue是一个接口,他的实现类主要包括以下几种:

类名 说明 ArrayBlockingQueue 基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。 LinkedBlockingQueue 基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。 DelayQueue DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。 PriorityBlockingQueue 基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),但需要注意的是PriorityBlockingQueue并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁。 SynchronousQueue 一种无缓冲的等待队列,类似于无中介的直接交易,有点像原始社会中的生产者和消费者,生产者拿着产品去集市销售给产品的最终消费者,而消费者必须亲自去集市找到所要商品的直接生产者,如果一方没有找到合适的目标,那么对不起,大家都在集市等待。和其他队列不同的还有,SynchronousQueue直接使用CAS实现线程的安全访问。主要使用场景是在线程池中,后续会讲到。

可参考:BlockingQueue(阻塞队列)详解

优先级队列

优先级队列和通常的栈和队列一样,只不过里面的每一个元素都有一个 优先级,在处理的时候,首先处理优先级最高的。如果两个元素具有相同的优先级,则按照他们插入到队列中的先后顺序处理。

可参考:Java Queue系列之PriorityQueue

总结

栈和队列涉及的方面很广,比如JVM的虚拟机栈,消息中间件MQ和队列的关系,本文主要强调对 栈和队列概念的理解,后续会逐步分享。

参考

拓展延伸

  • Java中的Stack是通过Vector来实现的,这种设计被认为是不良的设计,说说你的看法?
  • LIFO和FIFO各代表什么含义?
  • 栈的入栈和出栈操作与队列的插入和移除的时间复杂度是否相同?

Original: https://www.cnblogs.com/javatv/p/15614694.html
Author: Ayue、
Title: 栈和队列

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/572686/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 公众号文章汇总

    JDK源码分析实战系列-ThreadLocal自旋锁-JUC系列Doug Lea文章阅读记录-JUC系列AQS源码一窥-JUC系列AQS源码二探-JUC系列AQS源码三视-JUC系…

    Java 2023年6月14日
    086
  • 设计模式之观察者模式

    观察者模式是极其重要的一个设计模式,也是我几年开发过程中使用最多的设计模式,本文首先概述观察者模式的基本概念和Demo实现,接着是观察者模式在Java和Spring中的应用,最后是…

    Java 2023年6月8日
    0103
  • java技术难点

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    Java 2023年5月29日
    085
  • Java入门到精通——基础篇之多线程实现简单的PV操作的进程同步

    一、概述 PV操作是对信号量进行的操作。进程同步是指在并发进程之间存在一种制约关系,一个进程的执行依赖另一个进程的消息,当一个进程没有得到另一个进程的消息时应等待,直到消息到达才被…

    Java 2023年5月29日
    091
  • 接上篇:Git Worktree 高级使用,这样清爽多了

    前言 上一篇文章 Git Worktree 大法真香 带大家了解了 git worktree 是如何帮助我同时在多个分支工作,并且互不影响的。但是创建 worktree 的目录位置…

    Java 2023年6月5日
    081
  • springboot整合activity

    地址:https://blog.csdn.net/apm800/article/details/106112994 此博客只是为了记忆相关知识点,大部分为网络上的文章,在此向各个文…

    Java 2023年5月30日
    077
  • 必知必会之Lambda表达式

    Java是一门强大的面向对象的语言,除了 8种基本的数据类型,其他一切皆为对象。因此,在 Java中定义函数或方法都离不开对象,也就意味着很难直接将方法或函数像参数一样传递,而 J…

    Java 2023年6月6日
    085
  • springboot对LocalDateTime类型入参和接口返回值格式化

    背景 使用过java8的朋友应该都知道LocalDateTime类型,它作为全新的日期和时间API ,对比Date类型有着很大的优势,极大的方便了我们对于时间和日期的操作。不过,如…

    Java 2023年5月30日
    093
  • springboot集成swagger,出现 No mapping for GET /swagger-ui.html的错误

    在尝试降低springboot版本和swagger版本都无法解决这个问题后 在SpringBoot项目引入Swagger2后,在浏览器地址里输入地址:http://ip:port/…

    Java 2023年5月30日
    064
  • Java_深度剖析ConcurrentHashMap

    本文基于Java 7的源码做剖析。 多线程环境下,使用Hashmap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。虽然已经有一个…

    Java 2023年5月29日
    058
  • 25. Apache Shiro Java反序列化漏洞

    前言: 最近在审核漏洞的时候,发现尽管Apache shiro这个反序列化漏洞爆出来好久了,但是由于漏洞特征不明显,并且shiro这个组件之前很少听说,导致大厂很多服务还存在shi…

    Java 2023年5月29日
    098
  • 十进制转换为二进制,八进制,十六进制的简单实现

    public class Demo { public static void main(String[] args) { Scanner input = new Scanner(S…

    Java 2023年6月6日
    085
  • 接口和包–Java学习笔记

    interface定义:没有字段的抽象类 interface person{ void hello(); String getName(); } /*接口本质上就是抽象类 abst…

    Java 2023年6月8日
    0103
  • 设计模式 — FactoryMethod(工厂方法)

    工厂方法(Factory Method) 定义一个用于创建对象的接口,让子类决定实例化哪个类。Factory Method 使得一个类的实例化延迟(目的:解耦)到子类。 在软件系统…

    Java 2023年6月16日
    090
  • docker安装Java开发相关环境

    docker安装Java开发环境 官网:https://www.docker.com/ 主要安装: mysql:5.7.29 Redis RabbitMQ nginx 如果在获取镜…

    Java 2023年6月5日
    080
  • IDEA中tomcat插件版本7中文乱码问题

    tomcat插件版本7中文乱码问题 IDEA中tomcat插件版本7中文乱码问题 问题描述: 因为idea中tomcat插件版本只到7,他的默认解码方式为:ISO-8859-1,又…

    Java 2023年6月6日
    079
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球