数据结构与算法学习2---队列

不诗意的女程序猿不是好厨师~
【转载请注明出处,From李诗雨---https://blog.csdn.net/cjm2484836553/article/details/93889029】

源码地址见github:https://github.com/junmei520/DataStructureStudy/tree/master/src/datastructure/queue

文章看似很长,其实非常容易理解。

1.队列介绍

队列是一个有序列表,可以用数组或链表来实现。
队列遵循先入先出原则。先存入队列的数据先取出,后存入队列的数据后取出。
如图,可以把它理解为一个单向通道:


在这里插入图片描述

2.用数组模拟队列

2.1思路分析

数组模拟队列示意图

首先我们需要一个数组arr[]来存储队列中的数据,用maxSize来代表队列的最大容量。为了体现先入先出的原则,我们规定只能在数组的尾部(rear)添加数据,只能在数组的头部(front)取出数据,所以我们还需要定义两个变量front, rear分别来记录队列前后端的下标。

2.2 撸代码进行实践

①定义相关变量和构造函数。

首先我们定义一个ArrayQueue类,声明一些变量,以及一个有参构造器:

public class ArrayQueue {
    private int maxSize;//表示数组的最大容量
    private int front;//队列头
    private int rear;//队列尾
    private int[] arr;//用于存放队列数据的数组

    public ArrayQueue(int arrMaxSise){
        maxSize=arrMaxSise;
        arr=new int[maxSize];
        front=-1;// 指向队列头部,注意front是指向队列头的前一个位置.
        rear=-1;// 指向队列尾,指向队列尾的数据(即就是队列最后一个数据)
    }

}

②添加数据到队列中。

需要注意的是,在数据添加之前需要先进行判断,如果队列没有满,那rear就后移并赋相应的值;如果队列已满,则数据是加入失败的。

 /**
     * 添加数据到队列中
     */
    public void addQueue(int data) {
        //先判断队列是否满
        if (isFull()) {
            System.out.println("队列已满,无法添加数据");
            return;
        }
        rear++;//让rear后移
        arr[rear] = data;
    }

    /**
     * 判断队列是否满
     */
    private boolean isFull() {
        return rear == maxSize - 1;
    }

③从队列中取数据。

需要注意的是,在从队列中取数据之前,需要先判断队列是否为空,如果队列不为空,front后移取出对应数据;如果队列为空,则无法取出数据。

 /**
     * 从队列中取数据
     * @return 返回取出的数据
     */
    public int getQueue() {
        //先判断队列是否为空
        if (isEmpty()) {
            //抛个异常
            throw new RuntimeException("队列为空,无法取出数据");
        }
        front++;//front后移
        return arr[front];
    }

    /**
     * 判断队列是否为空
     */
    private boolean isEmpty() {
        return front == rear;
    }

④显示队列的现有数据。

注意:是从现有的头数据开始显示,即从【front+1】开始显示,而非【0】开始。

/**
     * 显示现有队列的所有数据
     */
    public void showQueue() {
        //先判空
        if (isEmpty()) {
            System.out.println("队列为空,没有数据可以显示");
            return;
        }

        //遍历数组,依次打印数据。注意是从现有的头数据开始显示,即从【front+1】开始显示。
        for (int i = front + 1; i < arr.length; i++) {
            System.out.printf("arr[%d]=%d\n", i, arr[i]);
        }
    }

⑤显示队列的头数据。

注意:
此处只是显示,并没有将数据取出。所以front的指向并没有进行移动。
由于front是指向队列头的前一个位置,所以【front+1】的位置才是队列的头。

     /**
     * 显示队列的头数据。
     * 注意:只显示并没有取出数据,所以front的指向位置并没有变。
     */
    public int showHeadData() {
        if (isEmpty()) {
            throw new RuntimeException("队列为空,没有数据可以显示");
        }
        //由于front是指向队列头的前一个位置,所以【front+1】的位置才是队列的头
        return arr[front + 1];
    }

⑥进行验证

下面我们新建一个ArrayQueueTest类,进行验证我们用数组模拟的队列是否正确。
思路:
我们创建一个规定容量的队列,然后通过键盘的输入,进行队列的添加,显示等操作。
具体代码如下:

/**
 * @author shiyu
 * @create 2019-06-27 16:16
 */
public class ArrayQueueTest {
    public static void main(String[] args) {
        //先创建一个最大容量为3的队列
        ArrayQueue queue = new ArrayQueue(3);
        //用于接收用户键盘输入的指令
        char order = ' ';
        Scanner scanner = new Scanner(System.in);
        boolean loop = true;
        //相关指令提示信息
        System.out.println("s(show): 显示队列");
        System.out.println("e(exit): 退出程序");
        System.out.println("a(add): 添加数据到队列");
        System.out.println("g(get): 从队列取出数据");
        System.out.println("h(head): 查看队列头的数据");
        while (loop) {
            order = scanner.next().charAt(0);//每次只接收一个字符
            switch (order) {
                case 's':
                    queue.showQueue();
                    break;
                case 'a':
                    System.out.println("请输入你要加入队列的数据");
                    int data = scanner.nextInt();
                    queue.addQueue(data);
                    break;
                case 'g':
                    int queueData = queue.getQueue();
                    System.out.println("取出的队列数据是:" + queueData);
                    break;
                case 'h':
                    int headData = queue.showHeadData();
                    System.out.println("队列的头数据为:" + headData);
                    break;
                case 'e':
                    scanner.close();
                    loop = false;
                    break;
            }
        }
    }
}

我的键盘输入的截图:


在这里插入图片描述

在这里插入图片描述

3.数组模拟队列存在的问题

好,到这里,我们已经使用数组实现了队列的功能。

但是还存在一些问题:

我们再来做一个测试,运行上面的代码。我们先添加三个数据进“队列”,然后全部取出,这个时候“队列”已经为空,此时再添加新数据到“空队列”中,是添加不成功的,会报“队列已满,无法添加数据”。


在这里插入图片描述

明明是“空队列”了,却不能再次添加数据进去,这是怎么回事呢?

这就要再次看看下面这张图了:


在这里插入图片描述

由于我们<向队列中添加数据>和<从队列中取数据>都是通过移动这两个标记。那当我们取数据时,只是将front不断地上移,移动过的蓝色区就被空了出来,但是却不能被再次使用了。

所以问题就出现了:
使用数组模拟队列,数组只能被使用一次,不能复用。

4.使用环形数组来解决问题

现在我们要解决,数组只能被使用一次,不能复用的问题,那我们的思路肯定时想让数组可以被复用。

其实解决这个问题,我们的核心是使用 %(取摸),也就是,我们把数组想像成是一个环形的,只要有新的空间出来,就可以继续放数据进去。

当然对这个问题的理解,我也是花费了很多工夫和心思的,也经过了一些试错后,才比较正切的理解这种做法的正确性和简洁性。

4.1撸代码来实现 环形数组模拟队列

好,现在我们就先一边撸代码一边来理解吧。如果一遍下来还是模糊,那不妨你自己再试一遍,这样慢慢的就可以理解了。

嗯,为了可以更深刻的理解,我自己亲自动手模拟了一下,请忽略它的丑,以下所有图请忽略它们的外在~~~

基本思路是这样的:


在这里插入图片描述

基本原理是:

  • 用一个数组来盛装数据,但是这个数组,有一个空间是用来做约定用的,不能装数据,并且它是可以动态变化的,如图中的阴影部分。(这个如果你不太理解没关系,继续往下看,当你把代码整个看完,这一点就豁然开朗了)
  • 仍需要两个指针,但是含义有所变化,front指向队列的头,rear指向队列尾的后一个位置。他们的初始值都是0。

好,上面我们只是说了个大概,现在我们用一个大小为4的具体数组为例,来做详细的演示:


在这里插入图片描述

【笑哭】上图是我的笔记,请忽略美观性,毕竟它真的帮助了我的理解。现在我们看了上图,基本已经知道了 往队列中添加数据的操作,下面就让我们用代码来实现吧!

①定义相关变量和构造函数。

public class CircleArrayQueue {
    private int maxSize;//表示数组的最大容量
    private int front;//front指向队列的头
    private int rear;//注意:rear是指向队列的尾的下一个位置
    private int[] arr;//用于存放队列数据的数组

    public CircleArrayQueue(int arrMaxSise) {
        maxSize = arrMaxSise;
        arr = new int[maxSize];
        //front,rear不用赋值,默认值都是0
    }
}

②队列空和队列满的判断

    /**
     * 判断队列是否为空
     */
    private boolean isEmpty() {
        return front==rear;
    }

    /**
     * 判断队列是否满
     * 当  (rear+1)%maxSize==front 时 ,队列满
     */
    private boolean isFull() {
        return (rear+1)%maxSize==front;
    }
    

③向队列中添加数据

    /**
     * 添加数据到队列中
     */
    public void addQueue(int data) {
        //先判断队列是否满
        if (isFull()) {
            System.out.println("队列已满,无法添加数据");
            return;
        }
        arr[rear] = data;
        //rear需要向后移动,为了发挥它的复用性并防止它越界,我们这里要取模
        rear=(rear+1)%maxSize;
    }

④从队列中取出数据

这个其实比较简单,思路是这样的:
(1). 首先我们要判断队列是否为空;
(2). 不空的情况下,可以取,由于front指向的就是队列的头,所以拿一个变量来接受arr[front]就是取出的数据;
(3). 紧接着front要后移,指向新的头,同样这里要取模,所以front=(front+1)%maxSize.
我们来看下代码实现:

    /**
     * 从队列中取数据
     *
     * @return 返回取出的数据
     */
    public int getQueue() {
        //先判断队列是否为空
        if (isEmpty()) {
            //抛个异常
            throw new RuntimeException("队列为空,无法取出数据");
        }
        int value=arr[front]; //需要临时变量来接收一下
        front=(front+1)%maxSize;//front需要后移,此处同样需要取模
        return value;
    }

显示队列的头数据就更简单了,此处一并给出代码:

 /**
     * 显示队列的头数据。
     * 注意:只显示并没有取出数据,所以front的指向位置并没有变。
     */
    public int showHeadData() {
        if (isEmpty()) {
            throw new RuntimeException("队列为空,没有数据可以显示");
        }
        //front---就是队列头对应的下标
        return arr[front];
    }

⑤ 遍历显示队列的所有数据

还有比较关键的一步等着我来完成了,那就是遍历显示队列的所有数据。
这里我们必须要知道队列中的实际数据的个数,前面的小结论中我们提到过,队列中的有效元素个数=(rear+maxSize-front)% maxSize.

对于这个公式的理解,我又要上图了:


在这里插入图片描述

所以遍历显示队列的代码我们现在就比较好理解了:

    /**
     * 队列中 装的 实际数据的个数
     */
    private int realSize(){
        return (rear+maxSize-front)%maxSize;
    }

    /**
     * 显示队列现有的所有数据,注意是从头数据开始显示
     */
    public void showQueue() {
        //先判空
        if (isEmpty()) {
            System.out.println("队列为空,没有数据可以显示");
            return;
        }

        //遍历数组,依次打印数据。注意是从现有的头数据开始显示
        for (int i = front; i < front+realSize(); i++) {
            System.out.printf("arr[%d]=%d\n", i%maxSize, arr[i%maxSize]);
        }
    }

⑥进行验证

我们仍使用之前的ArrayQueueTest类,进行验证我们用数组模拟的队列是否正确。

public class ArrayQueueTest {
    public static void main(String[] args) {
        //测试数组模拟队列
        //先创建一个最大容量为3的队列
        //ArrayQueue queue = new ArrayQueue(3);

        //测试环形数组来模拟队列
        //创建一个容量为4的数组,
        // 但是它实际最多只能装3个数据,因为有一个空间需要用来做约定。
        CircleArrayQueue queue =new CircleArrayQueue(4);

        //用于接收用户键盘输入的指令
        char order = ' ';
        Scanner scanner = new Scanner(System.in);
        boolean loop = true;
        //相关指令提示信息
        System.out.println("s(show): 显示队列");
        System.out.println("e(exit): 退出程序");
        System.out.println("a(add): 添加数据到队列");
        System.out.println("g(get): 从队列取出数据");
        System.out.println("h(head): 查看队列头的数据");
        while (loop) {
            order = scanner.next().charAt(0);//每次只接收一个字符
            switch (order) {
                case 's':
                    queue.showQueue();
                    break;
                case 'a':
                    System.out.println("请输入你要加入队列的数据");
                    int data = scanner.nextInt();
                    queue.addQueue(data);
                    break;
                case 'g':
                    int queueData = queue.getQueue();
                    System.out.println("取出的队列数据是:" + queueData);
                    break;
                case 'h':
                    int headData = queue.showHeadData();
                    System.out.println("队列的头数据为:" + headData);
                    break;
                case 'e':
                    scanner.close();
                    loop = false;
                    break;
            }
        }
    }
}

运行结果截图:
添加数据,只能添加3个数据,加到第四个时,提示已满:


在这里插入图片描述

显示添加的数据:


在这里插入图片描述

好,说明添加没有问题。

下面我们来验证它的复用性,先取出一个数据,再添加,看是否可以添加成功:


在这里插入图片描述

取出了一个数据10,又成功加入了一个数据50,通过显示确实可以复用了。

再次梳理环形数组模拟队列的思路和要点

不要先我啰嗦,让我再把前面的东西都串起来再理理思路:

  • ①我们需要一个数组来盛装数据,但是这个数组,有一个空间是用来做约定用的,不能装数据,并且它是可以动态变化的,如之前图中的阴影部分。
  • ②我需要两个指针,front和rear。front指向队列的头,rear指向队列尾的后一个位置。他们的初始值都是0。
  • ③我们需要知道一些小结论:
    队列的最大容积=maxSize-1(因为有一个空间不能装数据);
    队列为空时:front==rear;
    队列满时: (rear+1)%maxSize ==front;
    队列的有效个数为:(rear+maxSize-front)%maxSize;
  • ④向队列中加数据,头不动,rear需要“后移”一位,这里需要注意,由于现在时环形可复用的队列,所以不是简单的rear++,而是要使用取模的方式 rear=(rear+1)%maxSize;
  • ⑤从队列中取出数据时,front需要后移一位,同样需要注意
    front=(front+1)%maxSize;
  • ⑥遍历队列时,应该从front开始,以(front+realSize)结束,对应的数组下标时 i%maxSize,注意要取模。

好,到这里,我已经对用数组模拟队列,和用环形数组模拟队列,都掌握了。不知道你理解了没有【笑哭】。

这次的感悟就是,动手乱画是个理解知识的好方法~

源码地址见github:https://github.com/junmei520/DataStructureStudy/tree/master/src/datastructure/queue

积累点滴,做好自己~

你可能感兴趣的:(数据结构与算法学习2---队列)