(希望我所描述的,给你带来收获!)
队列是先进先出的线性表,在具体应用中通常用链表或者数组来实现!队列结构可以类比日常生活中"排队买东西",在队伍末端的人可以看成新插入的元素,把排队买东西的整个过程看作是入队出队操作,那么总是排在最末尾的那个人最后买东西、最后一个交易完再“出队”!先进先出也可以换一种说法叫——后进后出。都是一个道理。
我们使用数组来实现我们的队列,因为有动态数组的基础,我们实现的队列不再是固定容量的——动态数组篇章的传送门:动态数组的实现
第一步:创建Queue接口,定义ArrayQueue的一般操作
1 public interface Queue{2 intgetSize();3 voidenqueue(E e);4 E dequeue();5 intgetCapacity();6 booleanisEmpty();7 }
主要的两个操作是 enqueue(入队)和dequeue(出队),我们的标准是,以动态数组尾部为队列尾~以动态数组首位Array[0]位置为队列首,为了保证队列结构的特性,我们不提供给用户查看队列中间元素的操作!
第二步:新建ArrayQueue,实现Queue接口的行为
1 public class ArrayQueue implements Queue{2
3 Arrayarray;4
5 public ArrayQueue(intcapacity) {6 array = new Array(capacity);7 }8
9 publicArrayQueue() {10 this(10);11 }12
13 @Override14 public intgetSize() {15 returnarray.getSize();16 }17
18 @Override19 public voidenqueue(E e) {20 array.addLast(e);21 }22
23 @Override24 publicE dequeue() {25 returnarray.removeFirst();26 }27
28 @Override29 public intgetCapacity() {30 returnarray.capacity();31 }32
33 @Override34 public booleanisEmpty() {35 returnarray.isEmpty();36 }37 }
对于ArrayQueue的一些操作,出队操作的时间复杂度总是O(n)的、其他操作的均摊均为O(1)级别(关于均摊复杂度和震荡复杂度会另起一篇介绍)。出于对出队操作的复杂度考虑,我们不希望有如此高昂的时间代价,我们可以基于数组实现循环队列来降低该操作成本!
实现循环队列的第一步:实现Queue接口>
1 private E[] data; //存储数据的数组
2 private int front; //队列头
3 private int tail; //队列尾元素的下一个位置
4 private int size; //记录数据总长度
5 public LoopQueue(intcapacity) {6 data = (E[])new Object[capacity + 1];7 }8
9 publicLoopQueue() {10 this(10);11 }
分别声明了front、tail、size三个变量分别用来记录队列首位置、队列尾位置的下一个空位置和数据总长度
值得思考的是:
第一:tail的实际index值(数组下标值,往后统一使用index替换)是否总是大于front的实际index值?(要考虑循环)
第二:front == tail 应该作为判定队列是否为空的标志,那队列满的标志,front和tail的关系如何?
第二步:
1 public class LoopQueue implements Queue{2 private E[] data; //存储数据的数组
3 private int front; //队列头
4 private int tail; //队列尾
5 private int size; //记录数组总长度
6 public LoopQueue(intcapacity) {7 data = (E[])new Object[capacity + 1];8 }9
10 publicLoopQueue() {11 this(10);12 }13
14
15 @Override16 public intgetSize() {17 returnsize;18 }19
20 @Override21 public voidenqueue(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(intnewCapacity) {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 @Override40 publicE 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 returne;51 }52
53 @Override54 public intgetCapacity() {55 return data.length - 1;56 }57
58 @Override59 public booleanisEmpty() {60 return tail ==front;61 }62
63 /**
64 * 用于测试的toString方法65 *@return
66 */
67 @Override68 publicString toString() {69 StringBuilder str = newStringBuilder();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 returnString.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的操作