接上一篇继续往下挖,在上一篇,我们实现了一个属于自己的动态数组。利用这个动态数组,我们来实现一个基于动态数组,一个属于自己的普通队列Queue。
Queue 是一种它许我们从表的一段进行删除,表的另一端进行插入的线性表。可能也有人问啥叫线性表。
线性表:线性表是n个具有相同特性的数据元素的有限序列 (n>=0)。
n是线性表的长度,n=0就是空表,n>0时,表通常记作(a1,a2,a3...a_n)
除了a1外,表中每个元素 a[i] 均有唯一的前驱 a[i-1];
除了an外,表中每个元素 a[i] 均有唯一的后继 a[i+1];
线性表在逻辑上是线性结构,
认识完了Queue,我们再来认识下Java util包下,Doug Lea 实现的Queue接口各个方法的定义。然后我们尽可能向JDK的源码靠近。
package java.util;
public interface Queue<E> extends Collection<E> {
//添加一个元素,成功时返回true, 没有空间则报一个非法状态异常。
boolean add(E e);
//也时添加元素,比add更友好,add只给你异常,不给你false,offer给你返false。
boolean offer(E e);
//检索并删除此队列的头。此方法与poll的不同之处在于,它在队列为空时抛出异常。
E remove();
//检索并删除此队列的头,如果此队列为空则返回null。
E poll();
//检索但不删除此队列的头。此方法与peek的不同之处在于,它在队列为空时抛出异常。
E element();
//检索但不删除此队列的头,如果此队列为空则返回null。
E peek();
}
全部翻译了一遍,相信看出来了,add,offer都是添加元素,remove和poll也都是删除首元素,element和peek也都是查看首元素。唯一的区别是,前者给你异常,想停掉代码流;而后者给你null,让你自己处理。
add,remove,element ===>均异常,offer,poll,peek ===> 均给null。
这个接口还继承了 Collection 接口,Collection自然会对多种底层实现的集合容器实现一个统一的迭代器,迭代器这块1.8之后又加了一些实用defaut方法,这些就不关注了。主要关注下面2个方法:
size方法,返回容器中元素个数,isEmpty方法判断是否为空。
把以上抛异常的三个方法 add,remove,element 做个整合,我们推出以下接口,尽量来靠近JDK。
package com.company.queue;
/**
* 队列
*
* @author <a href="610495444@qq.com">wang da wei</a>
* 2021/11/7 0:24
* @version 1.0.0
*/
public interface Queue<E> {
/**
* 入队
* @param e
*/
void add(E e);
/**
* 出队
* @return 出队的元素
*/
E remove();
/**
* 查看队首的元素。
* @return 队首元素。
*/
E element();
/**
* 是否是空的。
* @return 是否为空。
*/
boolean isEmpty();
/**
* 查看线性表中元素个数。
* @return 元素个数
*/
int size();
}
有了这种接口,我们就可以做2种实现了。先做一个普通的队列,再做一个循环队列。
基于上一篇的动态数组,我们很容易的就可以实现出一个FIFO的队列。代码如下:
package com.company.queue.common;
import com.company.array.DynamicArray;
import com.company.queue.Queue;
public class ArrayQueue<E> implements Queue<E> {
private DynamicArray<E> dynamicArray ;
public ArrayQueue() {
this.dynamicArray = new DynamicArray<>();
}
@Override
public void add(E e) {
//添加到尾部 即 arr[size]位置。
dynamicArray.add(e,dynamicArray.getSize());
}
@Override
public E remove() {
//移除头部元素,并返回。
return dynamicArray.remove(0);
}
@Override
public E element() {
//查看第一个元素。
return dynamicArray.get(0);
}
@Override
public boolean isEmpty() {
//查看是否为空
return dynamicArray.getSize() == 0;
}
@Override
public int size() {
//获取动态数组中实际元素的个数。
return dynamicArray.getSize();
}
}
再来一个循环队列的实现:
package com.company.queue.loop;
import com.company.queue.Queue;
public class LoopQueue<E> implements Queue<E> {
private E[] data;
private int front, tail;
private int size; // 思考:不用 size 变量,如何实现?
//如果用户知道最多为循环队列传递多少元素的时候,可以直接传入一个capacity.
public LoopQueue(int capacity) {
data = (E[]) new Object[capacity + 1];
front =0;
tail = 0;
size = 0 ;
}
public LoopQueue(){
this(10);
}
public int getCapacity(){
return data.length -1;
}
@Override
public void add(E e) {
if( (tail +1 )% data.length == front ){
resize(getCapacity() * 2);
}
data[tail] =e;
tail = (tail + 1) % data.length;
size ++;
}
private void resize(int newCapacity) {
E[] newData = (E[]) new Object[newCapacity +1];
for(int i = 0 ;i< size ;i++){
int old_index = (i + front) % data.length;
newData[i] = data[old_index];
}
data = newData;
front = 0;
tail =size;
}
@Override
public E remove() {
if(isEmpty() ) throw new IllegalArgumentException("cannot dequeue from an empty queue .");
E ret = data[front];
data[front] = null;
front = (front + 1) % data.length;
size--;
return ret;
}
@Override
public E element() {
if(isEmpty() ) throw new IllegalArgumentException("cannot dequeue from an empty queue .");
return data[front];
}
@Override
public boolean isEmpty() {
return front == tail;
}
@Override
public int size() {
return size;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder(String.format("Queue : size/capacity = %d/%d \nfront [ ",size,getCapacity()));
for(int i = 0;i != tail; i = (i+1)% data.length){
sb.append(data[i]);
//当前索引不是最后一个元素。
if((i+1)%data.length != tail ){
sb.append(" , ");
}
}
sb.append(" ] tail ");
return sb.toString();
}
public static void main(String[] args) {
LoopQueue<Integer> queue = new LoopQueue<>();
for(int i = 0 ;i<20;i++){
queue.add(i);
System.out.println(queue);
if(i%3 == 2){
queue.remove();
System.out.println(queue);
}
}
}
}
两种队列的性能比较:
package com.company.queue;
import com.company.queue.common.ArrayQueue;
import com.company.queue.loop.LoopQueue;
import java.util.Random;
public class MainQueueTest {
public static void main(String[] args) {
int opCount = 10_0000;
ArrayQueue<Integer> arrayQueue = new ArrayQueue<>();
double arrayQueueTime = testQueue(arrayQueue, opCount);
System.out.println(arrayQueueTime);
LoopQueue<Integer> loopQueue = new LoopQueue<>();
double loopQueueTime = testQueue(loopQueue, opCount);
System.out.println(loopQueueTime);
}
public static double testQueue(Queue queue,int opCount){
long start = System.nanoTime();
Random random = new Random();
for(int i = 0 ;i<opCount;i++){
queue.add(random.nextInt(Integer.MAX_VALUE));
}
for(int i = 0 ;i<opCount;i++){
queue.remove();
}
long end = System.nanoTime();
return (end- start)/10_0000_0000D; //纳秒 : 9个0. 不要忘了加D,或者 后缀 .0
}
}
由于循环队列出队操作是O(1)的时间复杂度,所以当数据量越大的时候,一般有更好的性能优势。