一、 队列的概念
首先要知道队列的一个特性:有序性。
我们在接触数据结构的时候,会很轻松发现不同的数据结构之间的不同点之一:存储和使用数据的方式不同。
比如上一篇介绍的稀疏数组,其本质是一个数组,存储时指定索引然后赋值,获取时指定索引取值。
而这次介绍的队列不同,队列的储存取值特点是:先入先出
即先存入的值会被先取出,后存入的值只能在先存入的值取出之后取出
概念比较简单,那么接下来让我们用数组模拟实现一下队列。
二、 用数组模拟队列
1. 简单模拟
模拟示意图
设置几个变量
maxSize :队列的最大容量
front :指向第一个数据的前一个地址,如果队列为空则指向 -1(因为第一个元素没有)
rear :指向最后一个数据(队列尾)
思路分析
设置了变量之后,考虑几个问题:
1 . 什么时候队列为空?
答:front == rear 的时候
2 . 什么时候队列满
答:当 rear == maxSize - 1 的时候,因为数组是用0开头的,而maxSize是从1开始算的,所以要 -1
代码实现
1 . 首先用数组实现一个ArrayQueue类,模拟实现队列。
因为没什么难点,所以主要的过程以注释的形式解释。
// 用数组模拟一个队列
class ArrayQueue {
private int maxSize;// 最大的存储长度
private int front;// 头部指针
private int rear;// 尾部指针
private int[] queue;// 模拟队列的数组
// 构造函数,初始化最大存储量、头尾指针及queue
public ArrayQueue(int maxSize) {
this.maxSize = maxSize;
this.front = -1;
this.rear = -1;
this.queue = new int[maxSize];
}
// 判断队列是否为空
private boolean isEmpty(){
if (this.front == this.rear){
return true;
}
return false;
}
// 判断队列是否为满
private boolean isFull(){
if (this.rear == this.maxSize-1){
return true;
}
return false;
}
// 向队列中添加数据
public void add(int data){
// 先判断是不是满的
if(this.isFull()){
return;
}
// 如果不是满的,继续逻辑
rear++;
// 先++是因为,rear指向的是最后一个元素,如果rear后移则指向后一个空闲的位置
queue[rear] = data;
}
// 从队列中获取数据
public int get(){
// 先判断是不是空的
if (this.isEmpty()){
throw new RuntimeException("队列为空,无法取出数据");
}
// 如果不是满的,要遵守先入先出的规则
// 因为输入存放是从数组头部开始存放的
// 所以取的时候要利用front取值
front++;
return queue[front];
}
// 显示队列所有数据
public void showAll(){
if (this.isEmpty()){
throw new RuntimeException("队列为空,无法展示数据");
}
for (int data:queue){
if (data == 0){
break;
}
System.out.print(data + " ");
}
System.out.println();
}
// 显示队列头部数据(第一个数据)
public void showHead(){
if (this.isEmpty()) {
throw new RuntimeException("队列为空,无头部数据!");
}
System.out.println(queue[front + 1]);// front指向的是第一个数据的前一个位置
}
// 测试用
public void show(){
System.out.println(this.maxSize);
System.out.println(this.front);
System.out.println(this.rear);
System.out.println(this.queue);
}
}
1. 环形队列模拟
上面用数组简单模拟了一个队列,但是相信仔细的朋友,或者重复测试了一些次数后的朋友,肯定会发现,上面这个队列有个很严重的问题:它不能重复用。
这就不好了,难道每次用完还得再 new 一个吗?太麻烦了,所以思考一下解决方案。
想要解决一个问题,首先肯定得弄明白是什么导致的这个问题。
当我们用上面的例子,把队列填满,再把队列清空,发现不能继续填入数据了,很容易会发现,指针没有回退。
没有回退,第一个思路是,当满了之后,让指针重置回退,但这样会影响队列最大长度maxSize的波动,而且很不好弄,所以,需要另外提出一个方案:
将队列做成环形
那么难点是,怎么把一个线性的数组模拟成环形的???
关于将线性数组拟合成环形,不止一种算法,下面是我使用的一种。
思路分析
首先,需要重新定义一些东西:
-
front不再指向第一个元素的前一个位置了,而令front直接指向第一个元素。 也就是说,queue[front] 就是队列中的一个元素。
-
rear 不再指向最后一个数据,而是指向最后一个元素的后一个位置,这样就会在队列的最后空出一个位置,空出的最后一个位置,之后用到会说。rear的初值为0
-
那么,这样,队列满的时候,是什么条件??
答:当队列满时,条件是:( rear + 1 ) % maxSize = front
这个条件判别式是怎么得出来的?
先介绍一下取模运算:取模运算实际上相当于我们小学的时候学过的求余数。比如 5 % 3 = 2 , 3 % 5 = 3
-
举个例子:
如果队列的最大长度为5,那么索引为:0 1 2 3 4,空出一个,则实际上能够存储数据的索引为:0 1 2 3,
当 front 为0,rear为4(0123装满数据,rear根据定义需要指向最后一个数据的后一个位置,即为4)
rear+1 为 5 ,5模上5为0,等于front,此时队列为满。 -
在环形的情况下,举个例子:
如果队列的最大容量为11,那么索引为从0到10,假如 front 为6,队列满,由于队尾空了一个位置,所以5被空出来了,4是队尾,即最后一个数组,而我们定义 rear 是队尾的后一个位置,所以 rear 为5。(5+1)% 11 = 6 = front。所以可以看出,判别式成立。 -
在其中有几个小问题:
- 为什么要取模?
因为队列为环形,按照线性的思维,rear 总是在 front 的后面,但是环形情况下,rear 有可能比 front 更小 ,取模可以兼顾两种情况。 - 为什么定义的时候,要空出队尾的一个位置?
因为,如果不空出来,不管队列为空或者为满,都满足 rear == front ,这样就会出现判断的重合,导致冲突。(可以自己试试调整rear的定义然后不空出最后那一个空,最后都会导致队列空和队列满的条件冲突) - 取模还有什么别的好处?
不用重置rear和front的值。如果,队列满的条件使用:rear + front = maxSize ,那么就需要,在每次rear到达临界值的时候重新将rear的值从0开始排起。而取模就不一样了,取模可以不用每次超出范围之后从新确定索引。
- 为什么要取模?
-
-
队列为空的时候,是什么条件?
rear == front 这个时候为什么不取模?为什么不考虑rear比front大一整个maxSize?不太好说,想知道的话自己想想。
-
队列中的有效数据个数是多少?
( rear + maxSize - front )% maxSize 计算结果即为有效数据的个数
- 为什么要 (rear - front)加上maxSize之后模上maxSize?
因为如果rear比front小,减出来是负数,加上maxSize可以手动将rear提比front大。
- 为什么要 (rear - front)加上maxSize之后模上maxSize?
代码实现
class Queue {
private int front;
private int rear;
private int maxSize;
private int[] queue;
// 各个判断方法的结论条件在分析里面都有
public Queue(int maxSize) {
// 初始化
front = 0;
rear = 0;
this.maxSize = maxSize;
queue = new int[maxSize];
}
// 查看队列是否满了
public boolean isFull() {
return (rear + 1) % maxSize == front;
}
// 查看队列是否为空
public boolean isEmpty() {
return rear == front;
}
// 添加数据到队列
public void add(int data) {
if (isFull()) {
System.out.println("抱歉,队列已满,不可添加数据!");
return;
}
queue[rear] = data;
// 为什么要这么做?
// 当front不是0,而rear要超过maxSize的时候,+1,就会超出范围
// 如果按照环形的思路,当rear跨过尾端的时候,就应该从首端继续接上
// 此时应该重置rear的值,但是有个更科学的算法:取模,相当于减去一个或多个maxSize。
rear = (rear + 1) % maxSize;
}
// 获取数据,即出队列
public int get() {
if (isEmpty()) {
throw new RuntimeException("队列存储为空!无数据可取!");
}
int ValueForReturn = queue[front];
// 为什么取模?和上面一样,都是考虑到数值超出了maxSize后需要重置
front = (front + 1) % maxSize;
return ValueForReturn;
}
// 显示队列的所有元素
public void showAll() {
if (isEmpty()) {
throw new RuntimeException("队列为空!");
}
for (int i = front; i < front + (rear + maxSize - front) % maxSize; i++) {
System.out.print(queue[i] + " ");
}
}
// 显示队列的头数据
public void showHead() {
if (isEmpty()) {
throw new RuntimeException("队列为空!");
}
System.out.print(queue[front]);
}
}
三、封装类的测试
上面分别实现了线性的队列实现和环形队列的实现,都是用一个类进行的封装,为了方便大家测试,给出主函数的测试程序。
以测试环形队列为例:
public class CircleArrayQueue {
public static void main(String[] args) {
System.out.println("利用数组模拟一个环形数组");
Scanner scanner = new Scanner(System.in);
System.out.println("请设置队列的长度!");
int size = scanner.nextInt();
Queue queue = new Queue(size);
while (true) {
System.out.println("Type a to add data into the queue");
System.out.println("Type g to get data from the queue");
System.out.println("Type s to show all of the data in the queue");
System.out.println("Type h to show head data of the queue");
System.out.println("请输入指令操作队列");
char key = scanner.next().charAt(0);
switch (key) {
case 'a': {
System.out.println("请输入需要添加到队列的数据");
int input = scanner.nextInt();
queue.add(input);
break;
}
case 'g': {
System.out.println("获取到数据: " + queue.get());
break;
}
case 's': {
System.out.println("队列中的所有数据: ");
queue.showAll();
break;
}
case 'h': {
System.out.print("队列中的头数据: ");
queue.showHead();
System.out.println();
break;
}
}
}
}
}