不诗意的女程序猿不是好厨师~
【转载请注明出处,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.数组模拟队列存在的问题
好,到这里,我们已经使用数组实现了队列的功能。
但是还存在一些问题:
我们再来做一个测试,运行上面的代码。我们先添加三个数据进“队列”,然后全部取出,这个时候“队列”已经为空,此时再添加新数据到“空队列”中,是添加不成功的,会报“队列已满,无法添加数据”。
明明是“空队列”了,却不能再次添加数据进去,这是怎么回事呢?
这就要再次看看下面这张图了:
由于我们<向队列中添加数据>和<从队列中取数据>都是通过移动
所以问题就出现了:
使用数组模拟队列,数组只能被使用一次,不能复用。
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
积累点滴,做好自己~