循环队列的原理与代码实现
数组实现的普通队列低效的原因
- 在出入队操作时,可能会引发数组的缩容与扩容。时间复杂度为O(n)
- 在出队时,会将队首后的其他元素全部向前移位。时间复杂度为O(n)
以上就是数组实现的普通队列效率低下的原因,而下面所讲到的循环队列则摒弃了第2条的批量移位操作,通过队首指针的移动实现了更高效的出队操作
循环队列实现原理
- 入队与出队
循环队列使用首(front)尾(tail)两个指针分别指向队首元素的索引与队尾待插入位置的索引。
出队时仅需将 front 向后移动一位即可;而入队时直接将入队元素放置在 tail 的索引位置上,tail 向后移动一位即可。
无论是出队还是入队,front 或 tail 都将会向后移动,当这两个指针超过数组容积时,使其再次回到数组头部。
以上操作只要保证出队队非空,入队队非满即可。
- 队空与队满
通过牺牲一个数组空间来区分队空和队满这两种情况:
队空:front == tail
队满:(tail + 1) % data.length == front
其中,+ 1就是所谓的浪费一个数组空间。
可以理解为:
队空:可能发生在出队时,即当 front 向后移动与 tail 指向同一个位置时
队满:可能发生在入队时,即当 tail 向后移动直到移动到 front 的前一个位置时
- 扩容与缩容
扩容与缩容就是开辟一个新的数组,该数组长度大小为原数组 n 倍,将原数组的所有元素复制到新数组中的操作
在这个过程中,原数组的索引要从 front 开始
- 指针对长度取模运算
无论是出队操作还是入队操作,只要满足出队队非空,入队队非满这一条件,front 和 tail 在向后移动时,当触及数组尾部,经过对数组长度的取模运算,就会回到数组头部,也正是因为这一特性,所以才叫做循环队列。
循环队列的代码实现
/**
* 循环队列
* @param <E>
*/
public class LoopQueue<E> implements Queue<E> {
private E[] data; // 存储元素的数组
private int front, tail; // 首尾指针
private int size; // 队列内元素个数
public LoopQueue(int capacity) {
// + 1,因为循环队列会存在一个元素空间浪费的情况
data = (E[]) new Object[capacity + 1];
// 开始时,首尾指针都指向0的位置
// 入队时,仅需维护尾指针;出队时,仅需维护首指针
front = 0;
tail = 0;
size = 0;
}
public LoopQueue() {
this(10);
}
@Override
public void enqueue(E e) {
// 队列若满则扩容为原容积2倍
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 --;
// 若队列中元素个数等于容积的1/4,且容积不为0时,缩容为原容积一半
if (size == getCapacity() / 4 && getCapacity() != 0)
resize(getCapacity() / 2);
return ret;
}
private void resize(int newCapacity) {
E[] newData = (E[]) new Object[newCapacity + 1];
for (int i = 0; i < size; i++)
newData[i] = data[(i + front) % data.length];
data = newData;
front = 0;
tail = size;
}
public int getCapacity() {
return data.length - 1;
}
@Override
public E getFront() {
if (isEmpty())
throw new IllegalArgumentException("Queue is empty.");
return data[front];
}
@Override
public int getSize() {
return size;
}
@Override
public boolean isEmpty() {
return front == tail;
}
@Override
public String toString() {
StringBuilder result = new StringBuilder();
result.append(String.format("Queue: size = %d, capacity = %d\n", size, getCapacity()));
result.append("front [");
for (int i = front; i != tail; i = (i + 1) % data.length) {
result.append(data[i]);
if ((i + 1) % data.length != tail)
result.append(", ");
}
result.append("] tail");
return result.toString();
}
}
效率对比测试
返回队列出入队opCount个整型数据需要的时间
测试普通数组队列、循环队列、链表队列出入队 10000 个整型数据
最终运行结果如下