数据结构之Java实现底层Queue

队列是一种先进先出的线性数据结构,只能观察到队首元素,首先创建了一个Queue接口类如下:

public interface Queue<E> {  //队列接口
 int getSize();           //获取队列大小
 boolean isEmpty();       //判断队列是否为空
 void enqueue(E e);       //入队
 E dequeue();             //出对
 E getFront();            //获取队首元素
}

可以利用文章(https://blog.csdn.net/zhangjun62/article/details/82720572)实现的动态数组来实现底层队列的数据结构,具体过程如下:

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万次入队和出队的消耗时间测试,电脑测试结果为

差异结果有上百倍之多,这就说明循环队列确实是将出队的时间复杂度降低。

猜你喜欢

转载自blog.csdn.net/zhangjun62/article/details/82735123