队列是一种先进先出的线性数据结构,只能观察到队首元素,首先创建了一个Queue接口类如下:
public interface Queue<E> { //队列接口
int getSize(); //获取队列大小
boolean isEmpty(); //判断队列是否为空
void enqueue(E e); //入队
E dequeue(); //出对
E getFront(); //获取队首元素
}
实现的动态数组来实现底层队列的数据结构,具体过程如下:
public class ArrayQueue<E> implements Queue<E>{
private Array<E> array; //声明数组用于保存数据
public ArrayQueue() { //无参构造函数,以默认容量生成队列
array = new Array<>();
}
public ArrayQueue(int capacity) { //有参构造函数,以传入容量生成队列
array = new Array<>(capacity);
}
@Override
public int getSize() { //返回队列大小
return array.getSize();
} @Override
public boolean isEmpty() { //判断队列是否为空
return array.isEmpty();
}
public int getCapacity() { //返回队列容量,也就是底层数组容量
return array.getCapacity();
} @Override
public void enqueue(E e) { //入队,也就是向数组末尾加入元素
array.addLast(e);
} @Override
public E dequeue() { //出对,也就是向数组头部删除元素
return array.removeFirst();
} @Override
public E getFront() { //获取队首元素
return array.getFirst();
}
//重写toString方法,便于打印观察数据结构
@Override
public String toString(){
StringBuilder res = new StringBuilder();
res.append("Queue: ");
res.append("Front [");
for(int i = 0 ; i < array.getSize() ; i ++){
res.append(array.get(i));
if(i != array.getSize() - 1)
res.append(", ");
}
res.append("] Tail");
return res.toString();
}
}
由于上面这种实现队列的方式出队时间复杂度为O(n),入队是往数组末尾添加元素时间复杂度为O(1),因此可以利用循环队列解决出队时间复杂度为O(n)的问题。为了实现循环队列,不能够使用上面的动态数组来实现。循环队列需要front指向头,tail指向尾部下一个位置,队列为空时front与tail相等,出队一个,front往后挪一下,元素不动,当尾部添加满时,可以往数组前面空位置添加,直到(tail + 1) % data.length == front 表明队列已满应该扩容,实际上空出一个位置,因为直接用front与tail是否相等判断会无法避免排除队列为空的情况,所以用了tail + 1 与front判断队列是否为满(牺牲了一个位置),由于是循环队列,tail可能会跑到front前面,所以需要tail每次加1挪动后模上数组长度。下面是具体实现过程
public class LoopQueue<E> implements Queue<E>{
private E[] data; //声明泛型数组用于保存数组
private int front, tail; //声明队首和队尾指示
private int size; //声明队列尺寸大小
//有参构造函数,以传入容量大小+1(因为需要牺牲一个位置,所以要比用户输入容量大1)生成数组
public LoopQueue(int capacity) {
data = (E[]) new Object[capacity + 1];
front = 0;
tail = 0;
size = 0;
}
//无参构造函数,默认生成容量为10的数组
public LoopQueue() {
this(10);
}
//获取队列容量,由于在构造时加1,需要返回时减去1,展现给用户的是输入的容量
public int getCapacity() {
return data.length -1;
}
@Override
public boolean isEmpty() { //判断队列是否为空
return front == tail;
}
@Override
public int getSize() { //获取队列大小
return size;
}
//入队操作,利用自定义resize函数扩容,实现动态数据结构
@Override
public void enqueue(E e) {
//判断队列是否已满,已满就以两倍当前容量扩容
if((tail + 1) % data.length == front)
resize(getCapacity() * 2);
data[tail] = e; //向队尾添加相应元素
tail = (tail + 1) % data.length; //将tail指向下一个位置
size++; //维护size,将其加1
} //出队操作,利用自定义resize函数缩容,避免内存浪费
@Override
public E dequeue() {
//如果队列已空,再进行出队就抛出异常
if(isEmpty())
throw new IllegalArgumentException("Cannot dequeue from an empty queue.");
E ret = data[front]; //保存出队元素
data[front] = null;
front = (front + 1) % data.length; //将front往后挪一位
size--; //维护size, 将其减一
//如果数组大小已经等于容量的1/4 并且数组容量的1/2不等于0,将数组容量缩减到当前容量的1/2
if(size == getCapacity() / 4 && getCapacity() / 2 != 0)
resize(getCapacity() / 2);
return ret; //返回出对元素
}
//获取头部元素
@Override
public E getFront() {
if(isEmpty())
throw new IllegalArgumentException("Queue is empty.");
return data[front];
}
//扩容函数是内部功能函数,将其是私有化
private void resize(int capacity) {
//以传入容量大小生成新数组
E [] newData = (E[]) new Object[capacity + 1];
//从front开始遍历将队列数据复制到新数组中
for(int i = 0; i < size; i++) {
newData[i] = data[(i + front) % data.length];
}
data = newData; //原数组指向新数组
front = 0; //front指向0
tail = size; //tail指向末尾的下一个位置
}
//重写toString方法,便于打印队列数据结构
@Override
public String toString() {
StringBuilder res = new StringBuilder();
res.append(String.format("Queue: size = %d, capacity = %d\n", size, getCapacity()));
res.append("Front [");
for(int i = front; i != tail; i = (i+1) % data.length) {
res.append(data[i]);
if ( (i+1) % data.length != tail) {
res.append(", ");
}
}
res.append("] Tail");
return res.toString();
}
}
由于ArrayQueue与LoopQueue的出队时间复杂度不一致,一个是O(n),另一个是O(1),下面写了一个简单的测试程序比较了一下两者运行时间,测试代码如下
import java.util.Random;
public class Main {
public static void main(String[] args) {
int opCount = 100000;
ArrayQueue<Integer> arrayQueue = new ArrayQueue<>();
double time1 = testQueue(arrayQueue, opCount);
System.out.println("ArrayQueue comsuming time: " + time1);
LoopQueue<Integer> loopQueue = new LoopQueue<>();
double time2 = testQueue(loopQueue, opCount);
System.out.println("LoopQueue comsuming time: " + time2);
}
public static double testQueue(Queue<Integer> q, int opCount) {
long startTime = System.nanoTime();
Random random = new Random();
for(int i=0; i < opCount; i++) {
q.enqueue(random.nextInt(Integer.MAX_VALUE));
}
for(int i=0; i < opCount; i++) {
q.dequeue();
}
long endTime = System.nanoTime();
return (endTime - startTime) / 1000000000.0;
}
}
每人电脑配置不一样,所以结果不一样,但是二者差异肯定会体现出来的,上面是两种队列分别进行10万次入队和出队的消耗时间测试,电脑测试结果为
差异结果有上百倍之多,这就说明循环队列确实是将出队的时间复杂度降低。