一、概念
顾名思义,就是用数组(array)去模拟队列(queue)。数组是有界的,所以会有数组容量(队列大小)和队列中实际元素个数两个变量,加上队列自带的队首和队尾指针,一共四个必要元素。
二、图解
1、描述
定义一个数组容量为4的数组队列。
2、入队
2.1、定义空队列
2.2、元素A入队
2.3、元素B入队
2.4、元素C入队
2.5、元素D入队
不难发现,队列满了,无法再添加了,这时候怎么办呢?两个思路:
1.入队的时候判断队列是否满,满了的话则抛出异常
2.入队的时候判断队列是否满,满了的话进行扩容(比如二倍扩容)
2.6、元素E入队
队列满,需扩容
2.7、入队小结
很简单,每次入队的时候只需要将队尾指针+1,然后将元素放入队列中即可。需要注意的就是每次入队需要判断队列是否满,满了的话可以扩容。下面一起看下出队示意图。
3、出队
3.1、描述
上述入队中的顺序是A->B->C->D-E,那么出队的时候也应该是A->B->C->D->E。先进先出嘛。
3.2、待出队的队列
3.3、元素A出队
3.4、元素BCDE出队
不难发现有个严重问题,空间浪费了,具体含义是索引位置为01234的空间都无法存放元素,这导致每次再入队的时候都会无脑扩容,却没利用上前面出队的空间。
3.5、出队小结
依旧很简单,每次出队的时候只需要将队首指针+1即可。问题:造成空间浪费。
4、阶段小结
综上所述(上述的出入队图解),可以总结以下几点:
1.队列为空判断条件:队首等于队尾
2.队列满判断条件:队尾等于capacity-1
3.入队:队尾++
4.出队:队首++
5.出队会造成空间浪费
三、coding
package com.chentongwei.struct.queue;
/**
* Description:
*
* @author TongWei.Chen 2019-11-21 15:42:29
*/
public class MyArrayQueueDemo1 {
public static void main(String[] args) {
ArrayQueue2 arrayQueue2 = new ArrayQueue2(2);
arrayQueue2.addAutoCapacity(1);
arrayQueue2.addAutoCapacity(2);
arrayQueue2.addAutoCapacity(3);
arrayQueue2.addAutoCapacity(4);
arrayQueue2.addAutoCapacity(5);
arrayQueue2.addAutoCapacity(6);
int value1 = arrayQueue2.removeAndGet();
System.out.println(value1);
int value2 = arrayQueue2.removeAndGet();
System.out.println(value2);
int value3 = arrayQueue2.removeAndGet();
System.out.println(value3);
int value4 = arrayQueue2.removeAndGet();
System.out.println(value4);
int value5 = arrayQueue2.removeAndGet();
System.out.println(value5);
arrayQueue2.show();
}
}
class ArrayQueue2 {
// 数组容量
private int capacity;
// 存放数据的数组
private int[] data;
// 队首指针
private int front;
// 队尾指针
private int tail;
public ArrayQueue2(int capacity) {
this.capacity = capacity;
data = new int[capacity];
// 我这里队首队尾初始化为-1,并不是说队列必须为-1,我个人习惯而已。
front = tail = -1;
}
/**
* 判断队列是否已满
*
* @return
*/
public boolean isFull() {
return tail == capacity - 1;
}
/**
* 判断队列是否为空
*
* @return
*/
public boolean isEmpty() {
return front == tail;
}
/**
* 入队操作,为实现扩容机制
* @param value:需要入队的值
*/
public void add(int value) {
if (isFull()) {
// 打log或throw new runtimeException
System.out.println("满了,没实现自动扩容");
return;
}
data[++ tail] = value;
}
/**
* 入队操作,已实现自动扩容机制
* @param value:需要入队的值
*/
public void addAutoCapacity(int value) {
if (isFull()) {
System.out.println("满了,将自动为您扩容~");
resize(2 * capacity);
}
data[++ tail] = value;
}
/**
* 出队操作,出队且返回出队的元素
* @return: 出队的元素
*/
public int removeAndGet() {
if (isEmpty()) {
throw new RuntimeException("都空了,别remove了");
}
return data[++ front];
}
/**
* 显示队列中的元素
*/
public void show() {
if (isEmpty()) {
System.out.println("空的");
return;
}
for (int value : data) {
System.out.printf("value=%d\n", value);
}
}
/**
* 数组扩容或者缩容(2倍/一半)
*
* @param newCapacity 新的容量
*/
private void resize(int newCapacity) {
// 生成新的数组
int[] newData = new int[newCapacity];
// 将原来数组中的元素,赋值到新的数组中
for (int i = 0; i < capacity; i++) {
newData[i] = data[i];
}
// 将原来的数组的引用指向新数组
data = newData;
// 将原来的数组的容量大小改为最新大小
capacity = newCapacity;
}
}
输出结果:
满了,将自动为您扩容~
满了,将自动为您扩容~
1
2
3
4
5
value=1
value=2
value=3
value=4
value=5
value=6
value=0
value=0
四、如何解决空间浪费的问题?
1、方案
- 方案一、出队时,将队列的数据整体向队首方向移动一位。
- 方案二、出队逻辑不变,而是再入队的时候进行移动。
2、图解方案一
2.1、现有如下队列
2.2、假设AB要出队
2.3、小结
每一次出队都要整体向前移动一次,意味着每一次出队的时间复杂度都为O(n),我们方案二更佳,所以直接舍弃此lowb的方法。
3、图解方案二
3.1、现有如下队列
3.2、假设A要入队
入队发现队列满了,所以向前移动tail-front次,若没满,则无须移动,效率远高于方案一。
2.4、小结
2.5、方案二coding
/**
* 入队操作,已实现自动扩容机制
* @param value:需要入队的值
*/
public void addAutoCapacity2(int value) {
if (isFull()) {
if (front == -1) {
System.out.println("满了,将自动为您扩容~");
resize(2 * capacity);
} else {
// 若后面没空间了,但是队首指针不在起始位,代表队首方向有位置可以填充。
// 数据搬移 tail - front 次
for (int i = front; i < tail; ++ i) {
// 整体向前移动,为什么是data[i + 1],而不是data[i]?因为i是front,front下一个是有效数据。可以看我3.1的图示
// 这还不明白的话自己画图梳理下,还不明白的话留言给我。
data[i - front] = data[i + 1];
// 赋值初始值,就是说你将A元素往前移动了一位,那么A元素原始位置要清空
data[i + 1] = 0;
}
// 数据版已完成后需要更新head和tail
// 为什么要-1?因为else结束后有++tail,所以这里需要先-1,否则越界了。
tail = tail - front - 1;
front = -1;
}
}
++ tail;
data[tail] = value;
}