上篇文章介绍了队列这个数据结构,发现普通数组队列,它的出队操作时间复杂度是O(n)的,因为数组第一个元素被移除后,后面的所有元素都要往前挪,导致每次执行出队操作的都会进行多次元素前移的操作(如下图),为了优化出队操作的性能消耗,我们可以使用循环队列
为什么循环队列能够解决这种性能消耗的问题呢?我们来看一下循环队列的运行过程吧
入队操作
出队操作
根据上面三张图相比两种队列,普通队列第一个元素被移除后,后面的所有元素都往前挪,消耗性能较多,而循环队列维护了两个变量 Front 和 Tail,Front指向了循环队列的队首,每次移除队首元素后,Front就会指向后面一个元素,所以每次移除队首元素后,Front就会往后挪一个单位,当循环队列中所有元素都被移除后,Front和Tail会指向同一个位置,也就是说当Front=Tail时,循环队列为空,下面我们看下继续往循环队列中添加元素时,会是什么样的情况。
首先可以看出,继续往循环队列中添加元素时,Front依旧指向了队首,而Tail在最后一个位置添加元素后,重新指向了前面没有元素的位置,当Tail指向下标为2的位置时,不再添加元素,因为如果此时再次往循环队列中添加元素,会再次出现Front=Tail的情况,但是我们都知道Front=Tail是表示循环队列为空,为了不出现这种情况,刻意留一个空位,可以定义当Tail在Front前一个位置的时候,也就是Tail+1=Front代表循环队列已满,但是出现下图中的情况时,Tail+1并不是等于Front,所以Tail+1是不合理的,应该将Tail+1对循环队列的长度取余,拿下面的图举例,(Tail+1)%4=Front,Tail=3,Front=0,(Tail+1)%4也就等于0,所以(Tail+1)%4=Front代表循环队列已满是合理的。
下面看下代码实现
public class LoopQueue implements Queue {
private E[] data;
private int front;
private int tail;
private int size;
@SuppressWarnings("unchecked")
public LoopQueue(int capacity) {
/*
* 这里的capacity加1的原因是,在循环队列中, 我们会浪费一个空间,这样能存元素的数量会少一个,
* 所以这里我们基于传进来的容量+1,这样就可以储存期望的元素数量了
*/
data = (E[]) new Object[capacity + 1];
front = 0;
tail = 0;
size = 0;
}
public LoopQueue() {
this(10);
}
//入队
@Override
public void enqueue(E e) {
if((tail+1)%data.length==front)
resize(getCapacity()*2);
data[tail]=e;
tail=(tail+1)%data.length;
size++;
}
//出队
@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;
size--;
if(size==getCapacity()/4 && getCapacity()/2!=0)
resize(getCapacity()/2);
return ret;
}
@SuppressWarnings("unchecked")
private void resize(int newCapacity) {
E[] newData = (E[]) new Object[newCapacity+1];
for(int i=0;i
newData[i]=data[(i+front)%data.length];
}
data=newData;
front=0;
tail=size;
}
public int getCapacity() {
// 减一是因为我们初始化的时候进行了+1操作
return data.length - 1;
}
@Override
public E getFront() {
if(isEmpty())
throw new IllegalArgumentException("Queue is emtpy.");
return data[front];
}
@Override
public int getSize() {
return size;
}
@Override
public boolean isEmpty() {
return front == tail;
}
@Override
public String toString() {
StringBuilder res = new StringBuilder();
res.append(String.format("Queue: size = %d , capacity = %d\n", size, getCapacity()));
res.append("[");
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("]");
return res.toString();
}
}
通过对维护Front和Tail参数,就可以将队列的出队操作变成O(1)了。
我们用下面的测试代码比较一下ArrayQueue和LoopQueue的性能差距
输出结果
可以看出在10万次入队和出队的操作下LoopQueue比ArrayQueue的效率高出百倍,所以在通过循环队列优化性能是有意义的。