创建队列、塞值和拿值
当我们创建一个LinkedList的时候,就可以用来模拟队列,因为该集合里有大量操作首尾元素的方法,之后就可以在该队列里进行数据的添加和获取。
但是当我们使用数组来实现时,如何创建一个队列呢?最大值怎么确定?首尾初始值怎么确定?
这一步骤中我就有了一些疑惑,如果我把创建队列、添加队列数据和删除队列数据分别用三个方法来表示,那么队列的首尾值该怎么跟随着队列呢?难道是要把它存储到创建的队列中?似乎不太合理,但是如果是每次使用队列的时候单独确定首尾值然后再传递的话那也显得不太合理,毕竟这个应该是队列的一个属性,咦?写到这里我突然想到,既然是队列的属性,那么把它设计成一个类,然后创建队列对象不就行了?
之后又出现了一个疑惑,就是假如我把数据填满,rear此刻是最大值,然后从我头部拿掉几个,此时数据还能塞得进去吗?由于我塞数据的时候是以rear和最大值做的比较,所以会提示数组已满,但实际上我拿掉几个之后头部又空了,但是没办法从头部塞数据。要怎么办呢?
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class MyQueen {
private int[] content;
private int maxSize;
private int front = -1;
private int rear = -1;
private static final int DEFAULT_SIZE = 5;
public MyQueen() {
this.maxSize = DEFAULT_SIZE;
content = new int[DEFAULT_SIZE];
}
// 如果传递了异常数据怎么处理??
public MyQueen(int maxSize) {
this.maxSize = maxSize;
content = new int[maxSize];
}
public void addQueen(int value) {
if (rear >= maxSize - 1) {
System.out.println("队列已满,无法再添加数据,且本队列暂不提供扩容");
return;
}
rear++;
content[rear] = value;
}
public int getRearValue() {
if (front == rear) {
System.out.println("队列为空");
return 0; // 队列为空的时候应该怎么处理返回值呢?
}
front++;
return content[front];
}
@Override
public String toString() {
List<Integer> temp = new ArrayList<>(10);
for (int i = content.length -1; i >= 0; i--) {
temp.add(content[i]);
}
Object[] result = temp.toArray();
return Arrays.toString(result);
}
}
需要注意的是:rear指向的是尾部的数据,front指向的是头部数据的前一个位置。而且这里的取数据并不是说真的把数据取没了,而是模拟的队列,但实际中取过的头部数据还在数组中,所以在遍历显示的时候不能遍历整个数组,要知道队列的首尾序号是front + 1和rear。
我们对这一代码进行优化,优化点为:
当队列为空无法获取数据时,不能简单的返回一个数据,可以使用抛异常来进行处理。
public int getRearValue() {
if (front == rear) {
throw new RuntimeException("队列为空");
}
front++;
return content[front];
}
如果仅仅是上面的那种实现,则会出现假溢出的问题,即就算头部的数据在逻辑上被取出来了,但是如果尾部此时是满的,则无法添加数据,需要优化,即环形队列
环形队列
对于环形队列,我们需要有这样的一种思路,就是把数组想象成一个环,这样当末尾满的时候,就会再顺延到开头。
这种情况下,数组的空间就可以循环利用,但同时也会牺牲掉一个数组空间来作为代价。
既然是环形,那么在算法上就会涉及到一个取模%的问题,如何取模一会儿再说,先理解一下环形队列的实现思路
1、调整front指向第一个元素,即初始值为0;
2、调整rear指向最后一个元素的后一个位置,这就意味着,永远会有一个空间是没有数据的,单纯用来指示rear,初始值也为0;(这里要注意的是,front和rear的指向并没有定论,只要能够实现循环队列即可)
3、当队列满的时候(此时会有一个预留的空余空间),如何判断已经满了?
对于上面的图,综合看来可以使用(rear + 1)% maxSize == front来判断已经满了。但我又产生了一个疑问,front和rear是在0到3的范围内变化还是可以无限递增?如果是无限递增的话,那么根据公式来看,front是余数,不可能会超过maxSize,也就是说可以确定front是在0—3范围内变化,那么rear呢?其实rear也是在0—3范围之间的。
但是有一点我们需要注意,front和rear在每一次操作中都会增加,但是我们需要通过一些简单的公式来将他们的值限定在 0—3之间,即增加后对maxSize取模。
4、如何判断队列为空?依然使用front == rear来判断。
5、如何获取队列中有效数据的个数?使用(maxSize + (rear - front))% maxSize来获取即可,数据肯定不会超过maxSize - 1。
综合代码如下:
public class CircleQueen {
private int[] content;
private int maxSize;
private int front;
private int rear;
public CircleQueen(int maxSize) {
front = 0;
rear = 0;
this.maxSize = maxSize;
content = new int[maxSize];
}
// 判断队列是否为空
private boolean isEmpty() {
return front == rear; // 此处不要像下面一样写多余的代码
// if (front == rear) {
// return true;
// } else {
// return false;
// }
// return front == rear ? true : false;
}
// 判断队列是否满了
private boolean isFull() {
return (rear + 1) % maxSize == front; // 总结出的规律
}
// 添加数据。每次添加数据,下标都会增长,但是要通过取模限定在maxSize - 1的范围内
public void addToQueen(int value) {
if (isFull()) {
System.out.println("队列已满");
return;
}
content[rear] = value; // 先放值
rear = (rear + 1) % maxSize; // 取模限定下标
}
// 获取数据
public int getValue() {
if (isEmpty()) {
throw new RuntimeException("队列为空");
}
int value = content[front];
front = (front + 1) % maxSize;
return value;
}
// 打印队列数据
public void printQueen() {
int length = (rear - front + maxSize) % maxSize; // rear可能比front要小
for (int i = front; i < front + length; i++) { // 注意这里i的范围
System.out.println(content[i % maxSize]);
}
}
}