(希望我所描述的,给你带来收获!)
队列是先进先出的线性表,在具体应用中通常用链表或者数组来实现!队列结构可以类比日常生活中"排队买东西",在队伍末端的人可以看成新插入的元素,把排队买东西的整个过程看作是入队出队操作,那么总是排在最末尾的那个人最后买东西、最后一个交易完再“出队”!先进先出也可以换一种说法叫——后进后出。都是一个道理。
我们使用数组来实现我们的队列,因为有动态数组的基础,我们实现的队列不再是固定容量的——动态数组篇章的传送门:动态数组的实现
第一步:创建Queue<E>接口,定义ArrayQueue的一般操作
1 public interface Queue<E> { 2 int getSize(); 3 void enqueue(E e); 4 E dequeue(); 5 int getCapacity(); 6 boolean isEmpty(); 7 }
主要的两个操作是 enqueue(入队)和dequeue(出队),我们的标准是,以动态数组尾部为队列尾~以动态数组首位Array[0]位置为队列首,为了保证队列结构的特性,我们不提供给用户查看队列中间元素的操作!
第二步:新建ArrayQueue<E>,实现Queue接口的行为
1 public class ArrayQueue<E> implements Queue<E> { 2 3 Array<E> array; 4 5 public ArrayQueue(int capacity) { 6 array = new Array<E>(capacity); 7 } 8 9 public ArrayQueue() { 10 this(10); 11 } 12 13 @Override 14 public int getSize() { 15 return array.getSize(); 16 } 17 18 @Override 19 public void enqueue(E e) { 20 array.addLast(e); 21 } 22 23 @Override 24 public E dequeue() { 25 return array.removeFirst(); 26 } 27 28 @Override 29 public int getCapacity() { 30 return array.capacity(); 31 } 32 33 @Override 34 public boolean isEmpty() { 35 return array.isEmpty(); 36 } 37 }
对于ArrayQueue的一些操作,出队操作的时间复杂度总是O(n)的、其他操作的均摊均为O(1)级别(关于均摊复杂度和震荡复杂度会另起一篇介绍)。出于对出队操作的复杂度考虑,我们不希望有如此高昂的时间代价,我们可以基于数组实现循环队列来降低该操作成本!
实现循环队列的第一步:<新建一个LoopQueue<E>实现Queue<E>接口>
1 private E[] data; //存储数据的数组 2 private int front; //队列头 3 private int tail; //队列尾 4 private int size; //记录数据总长度 5 public LoopQueue(int capacity) { 6 data = (E[])new Object[capacity + 1]; 7 } 8 9 public LoopQueue() { 10 this(10); 11 }
分别声明了front、tail、size三个变量分别用来记录队列首位置、队列尾位置和数据总长度
值得思考的是:
第一:tail的实际index值(数组下标值,往后统一使用index替换)是否总是大于front的实际index值?(要考虑循环)
第二:front == tail 应该作为判定队列是否为空的标志,那队列满的标志,front和tail的关系如何?
第二步:
1 public class LoopQueue<E> implements Queue<E> { 2 private E[] data; //存储数据的数组 3 private int front; //队列头 4 private int tail; //队列尾 5 private int size; //记录数组总长度 6 public LoopQueue(int capacity) { 7 data = (E[])new Object[capacity + 1]; 8 } 9 10 public LoopQueue() { 11 this(10); 12 } 13 14 15 @Override 16 public int getSize() { 17 return size; 18 } 19 20 @Override 21 public void enqueue(E e) { 22 if ((tail + 1)%data.length == front) 23 resize(getCapacity()*2); 24 data[tail] = e; 25 tail = (tail + 1) % data.length; 26 size++; 27 } 28 29 private void resize(int newCapacity) { 30 E[] newData = (E[])new Object[newCapacity + 1]; 31 for (int i = 0; i < size; i++) { 32 newData[i] = data[(front+i) % data.length]; 33 } 34 front = 0; 35 tail = size; 36 data = newData; 37 } 38 39 @Override 40 public E dequeue() { 41 if (isEmpty()) 42 throw new IllegalArgumentException("dequeue is failed,Queue is empty"); 43 44 E e = data[front]; 45 data[front] = null; 46 front = (front + 1) % data.length; 47 size --; 48 if ((size - 1) == getCapacity()/4 && getCapacity()/2 != 0) 49 resize(getCapacity()/2); 50 return e; 51 } 52 53 @Override 54 public int getCapacity() { 55 return data.length - 1; 56 } 57 58 @Override 59 public boolean isEmpty() { 60 return tail == front; 61 } 62 63 /** 64 * 用于测试的toString方法 65 * @return 66 */ 67 @Override 68 public String toString() { 69 StringBuilder str = new StringBuilder(); 70 str.append(String.format("Queue:size = %d, capacity = %d\n",size,getCapacity())); 71 str.append("front ["); 72 for (int i = front; i != tail ; i = (i + 1) % data.length) { 73 str.append(data[i]); 74 if ((i+1) % data.length != tail) 75 str.append(","); 76 } 77 str.append("] tail"); 78 return String.valueOf(str); 79 } 80 }
tail的实际index值(数组下标值,往后统一使用index替换)是否总是大于front的实际index值?(要考虑循环)
答:tail的index值不总是大于front的index值,因为队列满足循环的效果,当数组尾部已经无法承载容量时,如果(0,front)之间有足够空间,依然可以回到front之前的空间去存储数据。
front == tail 应该作为判定队列是否为空的标志,那队列满的标志,front和tail的关系如何?
答:队列满的标志应该是(tail + 1)% data.length == front(取模运算),因为整个队列是循环的,若data.length == 9,tail == 8,front == 1时,我们的下一次enqueue(入队)操作会在tail位置上插入一个元素,插入元素之后tail的index应该更新为 0;(取模运算的魅力就在于此)。思考一下,插入一个新的元素之后tail的值为0,若此时我再插入一个新的元素,tail的值是否会更新为1(要注意了!front == 1)?答案是不会的,因为我们插入元素之前总该要(tail + 1)% data.length == front 判断队列是否满!(建议画图观察!言语描述难免不够深刻)
总结来看:tail位置上总是存储一个用户不可见的无关元素,只有当enqueue时,才会使得tail位置的元素有意义,然而插入新元素之后,tail又会改变为 tail = (tail+1) % data.length; 整体的设计使得实际容量capacity的数组只能容纳(capacity - 1)个元素,换句话说,需要浪费一个空间!
这也是为什么在初始化数组时将用户传进的capacity进行加1的操作