数据结构学习笔记(三):队列(queue)

目录

1 队列的结构形式与操作原则

2 两种顺序队列及其代码实现(Java)

2.1 简单队列

2.1.1 增删查操作的实现

2.1.2 简单队列存在的弊端

2.2 循环队列

3 链式队列及其代码实现(Java)

3.1 链式队列的设计思路

3.2 增删查操作的实现


1 队列的结构形式与操作原则

队列是在两端分别进行增删操作的线性表。对照栈的数据进出在同一端的特性,虽然队列的两端都是开放的,但是各自都只有一种功能,一个为数据的进口,另一个为数据的出口,像极了一条单行道。我们将队列中新增数据的一端定义为队尾(rear),将删除数据的一端定义为队头(front)。

队列的操作原则是先进先出(Fist In Fist Out, FIFO),即先进来的先出去,后进来的后出去。假如我们的“单行道”中依次开进6辆车,如下图:

荣威最先开进来,也最先开出去,它不出去否则其他车也出不去;红旗最后开进来,只能等所有前车都开出了才能出去。

队列的增删操作只能分别在队头和队尾进行,中间不允许有任何操作,而且一旦有错误的数据进入,几乎没有修正的余地。虽然后进先出的栈也不允许在中间增删元素,但是如果将错误数据压栈,还能在下一次压栈前及时弹栈取出错误数据。队列不提供这种机会,车一旦开进单行道,是不允许再倒出来的,只能硬着头皮排队。

队列根据存储方式分为两大类型——顺序队列和链式队列,前者的实现基于数组,后者的实现基于链表。

 

2 两种顺序队列及其代码实现(Java)

顺序队列有两种实现方式,一种是简单的实现,一种是循环的实现。

2.1 简单队列

简单顺序队列是相对与后面要实现的循环顺序队列来说的,用数组来模拟单向线性的数据增删方式,除非队列为空,队尾永远在队头之后。

2.1.1 增删查操作的实现

为减少内存的浪费和增加使用的灵活性,我们采用带有动态扩容和缩容功能的数组来实现。数组的第0个非空元素为队头元素,数组的最后一个非空元素为队尾元素。定义一个头指针front指向队头元素,尾指针rear指向队尾元素后面的null。队列为空时,front指针和rear指针是重合的;新增数据时,将数据添加到rear指针的位置,然后尾指针后移一位;删除数据时,front指针位置的值改为null,然后front指针后移一位。只要队列不为空,rear指针一定在front指针之后。

数据结构学习笔记(三):队列(queue)_第1张图片

我们在简单顺序队列中定义了一系列用于数据增删查的公共方法,包括:append()新增元素,pop()删除并取回元素,remove()删除不取回元素,peek()获取队头元素,last()获取队尾元素,getFromFront(int distance)和getFromRear(int distance)分别是根据与队头和队尾的距离查找任意位置的元素,printAll()从队头到队尾遍历并打印所有的元素。

自定义了两个异常类,分别为空队列异常(EmptyQueueException)和距离超出限制异常(DistanceOutOfBoundsException),分别针对删除方法和两个带参数的查找方法,代码从略。

MySimpleArrayQueue.java:准备工作,创建一个简单顺序队列的类

package com.notes.data_structure3;

import com.notes.data_structure2.DistanceOutOfBoundsException;

public class MySimpleArrayQueue {

    // 容量缩放一次的单位为8,初始值为9,给尾指针留一个位置,因为尾指针在队尾元素之后
    private T[] array = (T[]) new Object[9];
    int front = 0; // 头指针
    int rear = 0; // 尾指针

    /**
     * 从 队尾 添加元素
     * @param data
     */
    public void append(T data) {
        if(size()==array.length-1){
            expend(); // 队列元素数量达到数组容量减1,扩容数组,减1为的是给尾指针留一个空间
        }
        array[rear++] = data; // 在尾端(尾指针位置)新增数据,尾指针后移
    }

    /**
     * 删除 并 取回 队头元素
     * @return
     * @throws EmptyQueueException
     */
    public T pop() throws EmptyQueueException {
        if(isEmpty()) { // 如果队列为空,报出异常
            throw new EmptyQueueException("队列是空的");
        }
        T ele = peek(); // 取出队头元素
        array[front++] = null; // 将队头指针位置数据改为null,然后指针后移
        if(array.length-size()>8) {
            shrink(); // 数组的空余容量超过8,缩容数组
        }
        if(isEmpty()) { // 如果数组为空,队头指针和队尾指针回到0索引位置
            front = 0;
            rear = 0;
        }
        return ele;
    }

    /**
     * 删除 队头元素 且不取回
     * @throws EmptyQueueException
     */
    public void remove() throws EmptyQueueException {
        if(isEmpty()) { // 如果队列为空,报出异常
            throw new EmptyQueueException("队列是空的");
        }
        array[front++] = null;
        if(array.length-size()>8) {
            shrink(); // 数组的空余容量超过8,缩容数组
        }
        if(isEmpty()) { // 如果数组为空,队头指针和队尾指针回到0索引位置
            front = 0;
            rear = 0;
        }
    }

    /**
     * 获取 队头 元素
     * @return
     * @throws EmptyQueueException
     */
    public T peek() throws EmptyQueueException {
        if(isEmpty()) { // 如果队列为空,报出异常
            throw new EmptyQueueException("队列是空的");
        }
        return array[front]; // 队头元素在头指针的位置
    }

    /**
     *  获取 队尾 元素
     * @return
     * @throws EmptyQueueException
     */
    public T last() throws EmptyQueueException {
        if(isEmpty()) { // 如果队列为空,报出异常
            throw new EmptyQueueException("队列是空的");
        }
        return array[rear-1]; // 队尾元素在尾指针的前一个位置
    }

    /**
     * 查找任意位置元素,按照与队列 头部 元素的距离
     * @param distance 与队头的距离,从队头开始数第几个元素,0即队头本身
     * @return
     * @throws EmptyQueueException
     * @throws DistanceOutOfBoundsException
     */
    public T getFromFront(int distance) throws EmptyQueueException, DistanceOutOfBoundsException {
        if(isEmpty()) { // 如果队列为空,报出异常
            throw new EmptyQueueException("队列是空的");
        }
        int temp = front; // 可移动的指针,起始位置为头指针
        if(distance=0) {
            for(int i=0;i=0) {
            for(int i=0;i

SimpleArrayQueueDemo.java:模拟增删查操作

package com.notes.data_structure3;

import com.notes.data_structure2.DistanceOutOfBoundsException;

public class SimpleArrayQueueDemo {
    public static void main(String[] args) throws EmptyQueueException, DistanceOutOfBoundsException {
        MySimpleArrayQueue queue = new MySimpleArrayQueue();

        /**
         * 模拟 增 的操作,从队列 尾部 加入元素
         */
        queue.append("荣威");
        queue.append("长安");
        queue.append("东风");
        queue.append("比亚迪");
        queue.append("吉利");
        queue.append("红旗");
        // queue.printAll(); // 打印验证

        /**
         * 模拟 删 的操作,pop()和remove()两种方法
         */
        // 先进者先出,荣威 第一个进,第一个出
        queue.remove();
        // queue.printAll(); // 打印验证

        // 将第二个进入的 长安 取出,并让其重新排队
        String subject = (String) queue.pop();
        queue.append(subject);
        // queue.printAll(); // 打印验证

        /**
         * 模拟 查 的操作,peek(), last(), getFromTop(), getFromRear()
         * 目前队列里的元素包括:东风 比亚迪 吉利 红旗 长安
         */
        String head_ele = (String) queue.peek(); // 获取 队头 元素
        System.out.println(head_ele); // 东风
        String tail_ele = (String) queue.last(); // 获取 队尾 元素
        System.out.println(tail_ele); // 长安
        String ele1 = (String) queue.getFromFront(3); // 获取中间元素,与 队头 的距离为3
        System.out.println(ele1); // 红旗
        String ele2 = (String) queue.getFromRear(2); // 获取中间元素,与 队尾 的距离为2
        System.out.println(ele2); // 吉利
    }
}

2.1.2 简单队列存在的弊端

以上基于带有缩放机制的数组实现的队列在设计上还是有一些不合理的地方。由于队列先进先出的原则,数据的增删操作互不干扰,完全可以频繁地交替进行,如果程序有多个线程,增删还可以同时进行,就像现实中的单行道一样,每时每刻都有车驶入,有车驶出。

这样一来,简单实现方式就存在一个问题:对于队列底层的数组,删除操作会造成值为null的空闲空间。与此同时,由于新增操作只能在末端进行,rear指针达到数组末端后,为了加入更多数据不得不向后开辟新的空间(扩容)。在数组触发缩容条件之前,已经空出来的位置不能被使用。缩容机制正是为了减少这种资源浪费,然而如果我们把缩容条件设得太紧,比如一次只缩放1个单位的容量,很容易引发过于频繁的缩放操作,从而损耗程序的性能。

在实际应用中,新增操作整体上是快于删除操作的,新增的过程很简单,就是把数据存进去,放进来的数据往往在等待某种处理。想象一下,如果我们的“单行道”建在一个加油站里,车辆排着队等待加油,一辆车驶出单行道进入加油位,后车必须等待前车加完油才能驶出,在这个过程中车辆从道路末端驶入的速度远远大于加油的速度,那么我们就要不断“扩建”加油站的单行道,这是不现实的,也是不必要的。

我们给数组增加扩容机制,目的是打破数组的申明必须指定长度所带来的限制。其实这种限制对我们的“加油站”来说是一个很好的限流措施,加油站就那么大,单行道上只能放那么多车。采用这种思路就不能采用上面的单向线性的数据增删方式,因为不设定扩容机制的数组会引发假溢出现象。比如上面的代码所模拟的删除与再新增操作,依次取出“荣威”和“长安”后在数组中留下两个值为null的空闲位置,“长安”如果想重新到队尾排队,rear指针将无所适从:

数据结构学习笔记(三):队列(queue)_第2张图片

所谓假溢出,就是说对于队列的底层实现数组来说已经溢出了,rear指针失去了指向;但是对于队列本身来说,并没有发生溢出,因为元素的删除使得队列有了两个空余的位置,只是不能使用。

如何让删除时留下的空闲位置在新增时得到再次使用,循环队列是一个很好的解决方案。

 

2.2 循环队列

循环队列采用单向循环的方式组织队列中的数据,让每一个位置都能循环使用。和简单队列最直观的区别是,循环队列的rear指针既能在front指针之后,也能在其之前。

设计循环队列时,无需为rear指针专门留出一个null位置,当数据新增到数组的末端时,如果数组被存满,rear指针不动,同时抛出异常,以达到“限流”的目的;如果数组不满,就意味着数组头部有空位,rear指针移到0索引的位置,这就形成了循环

数据结构学习笔记(三):队列(queue)_第3张图片

代码实现时,取消了数组的扩容和缩容方法,在构造方法中设定队列的最大容量。由于数据的增删操作是在队列中循环进行的,因此无法通过尾指针和头指针的差值获得队列中的元素数量,所以定义了一个count属性在数据增删时记录队列中的元素个数。front指针和rear指针重合时数据为空,由于有了count属性,判断队列为空和队列已满的方法可以通过count实现。

定义的一系列增删查的公共方法中,由于循环队列无法通过与队头或队尾的相对位置查找元素,因此取消了这组方法。队列元素的遍历仍然是从队头遍历到队尾,不论front指针对应的下标是否为0。

MyRingArrayQueue.java:准备工作,创建一个循环顺序队列的类

package com.notes.data_structure3;

public class MyRingArrayQueue {

    private int size; // 队列的容量
    private T[] array; // 定义一个数组
    private int front = 0; // 定义一个头指针
    private int rear = 0; // 定义一个尾指针
    private int count = 0; // 记录元素个数

    /**
     * 构造方法,在此设定 队列 的容量
     * 为了给rear指针留位置,数组比队列要多一位
     * @param size
     */
    public MyRingArrayQueue(int size) {
        this.size = size;
        this.array = (T[]) new Object[size+1];
    }

    /**
     * 添加元素,rear指针后移一位
     * @param data
     * @throws FullQueueException
     */
    public void append(T data) throws FullQueueException {
        if(isFull()) { // 如果队列已满,抛出异常
            throw new FullQueueException("队列已经满了");
        }
        if(rear!=size) { // 如果没有rear指针没有到数组最后
            array[rear++] = data; //在尾端(尾指针位置)新增数据,尾指针后移
        }else {
            array[rear] = data; //在尾端(尾指针位置)新增数据
            rear = 0; // rear指针回到0索引位置,循环使用空闲位置
        }
        count++; // 队列元素数量增加一个
    }

    /**
     * 删除 并 取回 元素
     * @return
     * @throws EmptyQueueException
     */
    public T pop() throws EmptyQueueException {
        if(isEmpty()) { // 如果队列为空,抛出异常
            throw new EmptyQueueException("队列是空的");
        }
        T ele = peek(); // 取出队头元素
        array[front++] = null; // 将队头指针位置数据改为null,然后指针后移
        count--; // 队列元素数量减少一个
        return ele;
    }

    /**
     * 删除元素且不取回
     * @throws EmptyQueueException
     */
    public void remove() throws EmptyQueueException {
        if(isEmpty()) { // 如果队列为空,抛出异常
            throw new EmptyQueueException("队列是空的");
        }
        array[front++] = null; // 将队头指针位置数据改为null,然后指针后移
        count--; // 队列元素数量减少一个
    }

    /**
     * 获取 队头 元素
     * @return
     * @throws EmptyQueueException
     */
    public T peek() throws EmptyQueueException {
        if(isEmpty()) { // 如果队列为空,抛出异常
            throw new EmptyQueueException("队列是空的");
        }
        return array[front]; // 取出队头元素
    }

    /**
     *  获取 队尾 元素
     * @return
     * @throws EmptyQueueException
     */
    public T last() throws EmptyQueueException {
        if(isEmpty()) { // 如果队列为空,抛出异常
            throw new EmptyQueueException("队列是空的");
        }
        return array[rear-1]; // 取出队尾元素
    }

    /**
     * 遍历打印 队列 中的所有元素
     * @throws EmptyQueueException
     */
    public void printAll() throws EmptyQueueException {
        if(isEmpty()) { // 如果队列为空,抛出异常
            throw new EmptyQueueException("队列是空的");
        }
        for(int i=0;i

RingArrayQueueDemo.java:模拟增删查操作

package com.notes.data_structure3;

public class RingArrayQueueDemo {
    public static void main(String[] args) throws FullQueueException, EmptyQueueException {
        /**
         * 设定 队列 容量,“加油站”最多驶入 6 辆车
         */
        MyRingArrayQueue queue = new MyRingArrayQueue(6);

        /**
         * 模拟 增 的操作,从队列 尾部 加入元素
         */
        queue.append("荣威");
        queue.append("长安");
        queue.append("东风");
        queue.append("比亚迪");
        queue.append("吉利");
        queue.append("红旗");
        // queue.printAll(); // 打印验证

        /**
         * 模拟 删 的操作,pop()和remove()两种方法
         */
        // 先进者先出,荣威 第一个进,第一个出
        queue.remove();
        // queue.printAll(); // 打印验证

        // 将第二个进入的 长安 取出,并让其重新排队
        String subject = (String) queue.pop();
        queue.append(subject);
        // queue.printAll(); // 打印验证

        // 再新增一个 奇瑞
        queue.append("奇瑞");
        // queue.printAll(); // 打印验证

        /**
         * 模拟 查 的操作,peek(), last()
         * 目前队列里的元素包括:东风 比亚迪 吉利 红旗 长安 奇瑞
         */
        String head_ele = (String) queue.peek(); // 获取 队头 元素
        System.out.println(head_ele); // 东风
        String tail_ele = (String) queue.last(); // 获取 队尾 元素
        System.out.println(tail_ele); // 奇瑞
    }
}

 

3 链式队列及其代码实现(Java)

3.1 链式队列的设计思路

链式队列顾名思义就是基于链表实现的队列,用Java实现链式队列的方法和实现单向链表的方法是非常相似的,参见单向链表的笔记,链接在下面:

https://blog.csdn.net/weixin_45370422/article/details/116573863

之所以能够用实现单向链表的方法实现链式队列,源于我们对链式队列的设计思路。在我们的设想中,队列的rear指针指向最新进入队列的元素,front指针指向不存数据的头结点,头结点指向队头元素。设计头结点的目的是:当队列为空时,指针仍有所指向,不至于成为“野指针”。

数据结构学习笔记(三):队列(queue)_第4张图片

链式队列和顺序队列一样,当队列为空时,front指针和rear指针的指向相同;和顺序队列不同的是,顺序队列给rear指针留了一个空位,链式队列与之相反,给front指针留了一个空位。

回顾用Java模拟单向链表的过程,我们定义了一个头结点(headNode)。在链式队列的设想中,需要一个外部指针front指向这个头结点。然而,指针其实也是一个结点对象,那么为了简化代码,完全可以让front指针自己充当头结点,front.next指向队头结点,即让头指针的指针指向队头。同样地,单向链表中还定义了一个当前结点(currentNode),而当且结点正相当于链式队列中rear指针要指向的最新进入的元素,那么为了简化代码,也完全可以让rear指针自己充当队尾结点,rear.next指向null,即队尾(链尾)的指针是空指针,新增结点时,rear指针后移一位。

数据结构学习笔记(三):队列(queue)_第5张图片

这样一来,我们就可以用实现单向链表的方法来实现链式队列,增删操作分别通过调节front的指针(next)和rear的指针(next)来实现。基于队列不能在中间增删元素的原则,我们取消了单向链表中定义的在中间插入和删除元素的方法。

3.2 增删查操作的实现

MyLinkedQueue.java:准备工作,创建一个链式队列的类

package com.notes.data_structure3;

import com.notes.data_structure2.DistanceOutOfBoundsException;

public class MyLinkedQueue {

    private Node front = new Node(null); // 队头指针
    private Node rear = front; // 队尾指针
    private int count; // 用于统计队列中的元素数量

    // 定义一个结点类
    public class Node {
        // 结点的两个要素:数据和指针
        private T data;
        private Node next;

        // 构造方法,初始化data属性
        public Node(T data) {
            this.data = data;
        }

        @Override
        public String toString() { // 可供printAll()方法调用
            return "Node{" +
                    "data=" + data +
                    '}';
        }
    }

    /**
     * 向 队尾 增加元素
     * @param data
     */
    public void append(T data) {
        Node node = new Node(data);
        if(isEmpty()) { // 如果队列为空,front指针充当头结点
            front.next = node; // front的指针指向队头元素
        }
        rear.next = node; // rear的指针指向新结点
        rear = node; // 新结点为rear结点,正式加入队列,rear实现后移
        count++; // 队列元素数量增加一个
    }

    /**
     * 从 队头 删除元素 并取回
     * @return
     */
    public T pop() throws EmptyQueueException {
        if(isEmpty()) { // 如果队列为空,抛出异常
            throw new EmptyQueueException("队列是空的");
        }
        T ele = peek(); // 取出 队头 元素
        front.next = front.next.next; // front的指针指向队头元素的下一个元素
        count--; // 队列元素数量减少一个
        return ele;
    }

    /**
     * 从 队头 删除元素 不取回
     */
    public void remove() throws EmptyQueueException {
        if(isEmpty()) { // 如果队列为空,抛出异常
            throw new EmptyQueueException("队列是空的");
        }
        front.next = front.next.next; // front的指针指向队头元素的下一个元素
        count--; // 队列元素数量减少一个
    }

    /**
     * 获取 队头 元素
     * @return
     * @throws EmptyQueueException
     */
    public T peek() throws EmptyQueueException {
        if(isEmpty()) { // 如果队列为空,抛出异常
            throw new EmptyQueueException("队列是空的");
        }
        return (T) front.next.data; // 返回队头结点的数据值
    }

    /**
     * 获取 队尾 元素
     * @return
     * @throws EmptyQueueException
     */
    public T last() throws EmptyQueueException {
        if(isEmpty()) { // 如果队列为空,抛出异常
            throw new EmptyQueueException("队列是空的");
        }
        return rear.data; // 返回队尾结点的数据值
    }

    /**
     * 查找任意位置元素,按照与队列 头部 元素的距离
     * @param distance 与队头的距离,从队头开始数第几个元素,0即队头本身
     * @return
     * @throws EmptyQueueException
     * @throws DistanceOutOfBoundsException
     */
    public T getFromFront(int distance) throws EmptyQueueException, DistanceOutOfBoundsException {
        if(isEmpty()) { // 如果队列为空,抛出异常
            throw new EmptyQueueException("队列是空的");
        }
        if(distance=0){
            Node temp = front; // 可移动的指针,起始位置为头结点
            for(int i=0;i

LinkedQueueDemo.java:模拟增删查操作

package com.notes.data_structure3;

import com.notes.data_structure2.DistanceOutOfBoundsException;

public class LinkedQueueDemo {
    public static void main(String[] args) throws EmptyQueueException, DistanceOutOfBoundsException {
        MyLinkedQueue queue = new MyLinkedQueue();

        /**
         * 模拟 增 的操作,从 队头 加入元素,即 “先进”
         */
        queue.append("荣威");
        queue.append("长安");
        queue.append("东风");
        queue.append("比亚迪");
        queue.append("吉利");
        queue.append("红旗");
        // queue.printAll(); // 打印验证

        /**
         * 模拟 删 的操作,pop()和remove()两种方法
         */
        // 先进者先出,荣威 第一个进,第一个出
        queue.remove();
        // queue.printAll(); // 打印验证

        // 将第二个进入的 长安 取出,并让其重新排队
        String subject = (String) queue.pop();
        queue.append(subject);
        // queue.printAll(); // 打印验证

        /**
         * 模拟 查 的操作,peek(), last(), getFromTop(), getFromRear()
         * 目前队列里的元素包括:东风 比亚迪 吉利 红旗 长安
         */
        String head_ele = (String) queue.peek(); // 获取 队头 元素
        System.out.println(head_ele); // 东风
        String tail_ele = (String) queue.last(); // 获取 队尾 元素
        System.out.println(tail_ele); // 长安
        String ele1 = (String) queue.getFromFront(3); // 获取中间元素,与 队头 的距离为3
        System.out.println(ele1); // 红旗
        String ele2 = (String) queue.getFromRear(2); // 获取中间元素,与 队尾 的距离为2
        System.out.println(ele2); // 吉利
    }
}

 

你可能感兴趣的:(数据结构学习笔记,数据结构,java,队列)