数组模拟环形队列
队列特点
队列是一个有序列表,可以用数组或者链表来实现.
同时队列还遵循着先进先出的原则,即先入队的元素先出队,后入队的元素后出队.
只要保证从队首取数据,队尾放数据就可以保证先进先出.
实现思路
首先需要一个数组和两个指针来记录上次操作的元素的位置,front和rear,
front用来记录取出数据的位置,rear用来记录新增数据的位置.
当存入数据时,rear的值要加一,取出数据时front的值也要加一.
front和rear的初始值都是-1,即不指向任何一个元素,
front 在队列头的第一个元素的位置之前,rear在队尾的最后一个元素之后.
实现
package element.io.data.structure.d2;
/**
* @author 张晓华
* @date 2022-12-16
* 使用数组模拟队列
*/
public class ArrayQueue {
// 用来指向队列头,记录的是上次操作的元素的索引位置,默认值为-1,队列为空时不指向任何一个元素
private int front = -1;
// 用来指向队列尾,记录的也是上次元素的索引的为位置,默认值为-1,即空队列时不指向任何一个元素
private int rear = -1;
private int maxSize;
public ArrayQueue() {
this(3);
}
public ArrayQueue(int maxSize) {
this.maxSize = maxSize;
this.arr = new Object[maxSize];
}
// 数组,用来存储数据
private Object[] arr;
private boolean isFull() {
// 只要向队列中添加了数据,rear的值一定会发生变化,
// 并且maxSize代表的是数组的长度即容量,front和rear记录的是索引值,所以必须做-1处理
return rear == maxSize - 1;
}
private boolean isEmpty() {
// front 和 rear 在初始化时值都为-1,并且存储数据时rear的值会增加,取数据的时候front的值会变化,
// 如果二者的值相同就代表着队列为空
return front == rear;
}
public void showData() {
if (isEmpty()) {
System.out.println("队列为空");
return;
}
for (int i = 0; i < arr.length; i++) {
System.out.printf("arr[%d] = %s \n", i, arr[i].toString());
}
}
// 向队列存入数据
public void add(Object ele) {
if (isFull()) {
throw new RuntimeException("队列已满");
}
// 由于rear和front在初始化后都是指向队列尾或者队列头的前一个元素,因此在使用前必须要+1
this.arr[++rear] = ele;
}
// 从队列中取出数据
public Object get() {
if (isEmpty()) {
throw new RuntimeException("队列为空");
}
return arr[++front];
}
public Object head() {
if (isEmpty()) {
throw new RuntimeException("队列为空");
}
return this.arr[front +1 ];
}
}
问题
上面的实现方式很明显存在着比较大的缺陷,就是队列只能够使用一次,不能够重复使用,因此需要将其升级为环形队列
改进的思路
改进后的代码
package element.io.data.structure.d2;
/**
* @author 张晓华
* @date 2022-12-17
* 使用数组模拟环形队列,其实并不难,核心就是要保证front和rear的值不断的循环并且不能出现索引越界
*/
public class CircleArrayQueue {
// 指向数组的第一个元素
private int front;
// 指向数组的最后一个元素,预留一个元素的空间
private int rear;
// 数组的最大容量
private int maxSize;
// 存储数据的数组
private Object[] arr;
public CircleArrayQueue() {
this(3);
}
public CircleArrayQueue(int maxSize) {
this.maxSize = maxSize;
this.arr = new Object[maxSize];
}
/**
* 假设数组的容量为6,那么初始化完成之后,front=0,rear=0
* 队列的可用容量为 6-1,预留了一个空间
* 那么队列中最多可以存储5个元素,
* 当只存不取的时候,front=0,rear=5
* 5 - 0 == front,那么此时队列就已经满了
* 当存了4个元素,取了一个元素,front=1,rear=4
* 4 - 1 != front 队列没有满
* 当存了5个元素,取了五个元素 front =5,rear = 5
* front - rear != front 那么这个判断就失效了
*/
private boolean isFull() {
// 这种方式不可行,因为初始化后front和rear的值都为0,首次添加数据就直接判断队列满了
//return (rear - front) == front;
// 当存了五个元素,没有取出元素, (5 + 1)%6 == 0
return (rear + 1) % maxSize == front;
}
/**
* 当没有向对列中存入数据时,front=0,rear=0,front == rear
* 当向对列中存入了三个数据时,front=0,rear=3,front!=fear
* 当向对列中存入了五个数据,同时也取出了五个数据,front == rear
* 所以就只需要判断front和rear的值是否相等就可以了
*
* @return
*/
private boolean isEmpty() {
return front == rear;
}
public int size() {
//这样直接相减会出现负数的情况,当rear被重置之后,再开始取值就会出现rear小于front
//return rear - front;
// 这样写就可以避免负数的问题了,因为模运算的结果的符号取决于右边
return (rear - front + maxSize) % maxSize;
}
public void add(Object obj) {
if (isFull()) {
throw new RuntimeException("队列已满");
}
this.arr[rear] = obj;
// 这里这样写就是为了规避掉索引越界异常,如果只是rear++,
// 那么一直添加数据,rear的值就会没有限制的一直增长,必然会超过maxLength,
// 当 rear +1 == maxSize时,rear的值就会被重置为0,
// 当rear+1 < maxSize时是没有任何影响的, 3%6 = 3
rear = (rear + 1) % maxSize;
}
public Object get() {
if (isEmpty()) {
throw new RuntimeException("队列为空");
}
Object res = this.arr[front];
// front和rear一样,都需要在临界值时重置,否则必然会出现索引越界
front = (front + 1) % maxSize;
return res;
}
public void showData() {
if (isEmpty()) {
throw new RuntimeException("队列为空");
}
// 必须从front开始获取数据,否则就不是从队列头获取数据了
for (int i = front; i < front + size(); i++) {
// 但是此处就会出现i值大于数组长度的情况,所以需要调整一下i的值,
// i 小于maxSize,对i的值不会产生任何变化,i值大于maxSize,可以调整i的值为正确的值
System.out.printf("arr[%d]=%s \n", i, arr[i % maxSize].toString());
}
}
public Object head() {
if (isEmpty()) {
throw new RuntimeException("队列为空");
}
return this.arr[front];
}
}