一。背景
如果我们想删除队首的元素,这个时候,如果是上面提到的[数组队列],就会变成O(n)的复杂度,因为所有的元素都要向前移动一个单位。 但是我们想让这个操作的变成一个O(1)的复杂度,就需要 : 循环队列
二。想法
1. 我们删除了data[0]的元素,就剩下4个元素[b,c,d,e],但是我们不挪动其他元素的位置。
2. 这样我们可以在数组中记录队首的位置,记为front。 tail记录下一个新的元素存储的位置
3. 这个时候,当b元素被删除的时候,我们只需要维护front的指向:front++。
4. 所以这个时候就有了循环队列
三。循环队列的过程
1. 数组初始化的时候front和tail都为空,指向第一个元素。
所以只要front==tail,就说明队列为空。
2. 当添加元素的时候,只需要进行tail++。
3. 删除2个元素[a,b],然后添加3个元素[f,g,h],就变成了下图样子。当h添加了之后,我们的tail就不能++了,但是这个时候,队首空缺了2个元素,我们就可以利用这些空间。这就是循环队列的由来
4. 所以添加完h元素之后,tail的索引应该是0.所以应该是下图所示。
所以对于tail的计算应该是:tail = (队尾索引+1)%组长度
5. 当我们给data[0]添加一个元素之后,tail变成了1. 此时我们空出了一个位置data[1],是因为当如果这个元素被填充,tail就变成了2,而此时的front也等于2,就会造成tail==front.但是tail==front是队列为空的判断条件,我们不需要这个条件既表示队列为空,又表示队列为满。
所以定义队列为满的条件是:(tail+1)%数组长度==front。也就是特意空出一个元素。
这里特意用了求余,是因为这是一个循环队列,当front为0,tail为7的时候,这个数组也就满的。
四。循环队列的代码实现
1. Queue
public interface Queue<E> {
void enQueue(E e);
E deQueue();
E getFront();
int getSize();
boolean isEmpty();
}
2.LoopQueue
public class LoopQueue<E> implements Queue<E>{
private E[] data;
private int front,tail;
private int size;
(1) 先写简单的方法
public LoopQueue(int capacity){
//capacity+1,是因为我们后面要浪费一个位置
data = (E[])new Object[capacity+1];
front = 0;
tail = 0;
size =0;
}
public int getCapacity(){
return data.length-1;
}
public LoopQueue(){
this(10);
}
@Override
public int getSize() {
return size;
}
@Override
public boolean isEmpty() {
return front==tail;
}
(2)给循环队列添加元素
思想: 当数组满了之后,我们需要给数组扩容为原来的2倍。
/*
* 思想:给队列的末尾添加一个元素,当数组满了之后,我们需要给数组扩容为原来的2倍。
* */
@Override
public void enQueue(E e) {
if((tail+1)%data.length == front){
//这里不能用data.length,因为getCapacity和length之间有1的差距
resize(getCapacity()*2);
}
data[tail] = e;
tail = (tail+1) % data.length;//因为是循环队列
size++;
}
/*
* 思想:resize是将现有数组中的元素放入到另外一个数组中,所以就需要从头开始将元素拿出来循环放到新的数组
* 有两种方式可以实现数组元素的循环
* 1.用size/front和tail元素
* 2.只用front和tail元素
* */
private void resize(int newCapacity){
E[] newData = (E[])new Object[newCapacity+1];//注意,因为要浪费一个空间,所以这里+1
//方式一
for(int i=0; i<size; i++){
newData[i] = data[(i+front)%data.length];
}
//方式二[或者 for(int i=front; i!= tail; i=(i+1)%data.length) ]
for(int i = front; (i%data.length)!=tail; i++){
newData[(i-front+data.length)%data.length] = data[i];
}
data = newData;
front =0;//不要忘记给使front为0
tail = size;
}
(3)删除队首的元素
/*
* 当数组不为空的时候,从队列的头部删除一个元素。
* */
@Override
public E deQueue() {
if(isEmpty()){
throw new IllegalArgumentException("Cannot dequeue from an empty queue.");
}
E e = data[front];
data[front] = null;
front = (front+1)% data.length;
size--;
//为了防止频繁的扩容缩容的操作,这里判断条件设置为原来的1/4
//当数组删除到了数组的1/4的时候,我们缩容1/2.所以还剩1/4的空间,这时候添加元素就不不会立即触发resize.
if(size == getCapacity()/4 && getCapacity()/2 !=0){//getCapacity()/2 !=0是为了防止数组的容量为1的情况
resize(getCapacity()/2);
}
return e;
}
(4)获取队列的头
public E getFront() {
if(isEmpty()){
throw new IllegalArgumentException("Queue is empty.");
}
return data[front];
}
(5)重写toString()方法
@Override
public String toString(){
StringBuilder res = new StringBuilder();
res.append(String.format("Queue: size=%d, capacity = %d\n", size, getCapacity()));
res.append("front [");
//只有tail和tail来遍历数组
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();
}
3.进行测试代码
LoopQueue<Integer> queue = new LoopQueue<>();
for(int i=0;i<10;i++){
queue.enQueue(i);
System.out.println(queue);
if(i%3 == 2){
queue.deQueue();
System.out.println(queue);
}
}
4.测试结果
Queue: size=1, capacity = 10
front [0] tail
Queue: size=2, capacity = 10
front [0, 1] tail
Queue: size=3, capacity = 10
front [0, 1, 2] tail
Queue: size=2, capacity = 5
front [1, 2] tail
Queue: size=3, capacity = 5
front [1, 2, 3] tail
Queue: size=4, capacity = 5
front [1, 2, 3, 4] tail
Queue: size=5, capacity = 5
front [1, 2, 3, 4, 5] tail
Queue: size=4, capacity = 5
front [2, 3, 4, 5] tail
Queue: size=5, capacity = 5
front [2, 3, 4, 5, 6] tail
Queue: size=6, capacity = 10
front [2, 3, 4, 5, 6, 7] tail
Queue: size=7, capacity = 10
front [2, 3, 4, 5, 6, 7, 8] tail
Queue: size=6, capacity = 10
front [3, 4, 5, 6, 7, 8] tail
Queue: size=7, capacity = 10
front [3, 4, 5, 6, 7, 8, 9] tail
五。复杂度分析
- void enQueue(E e); O(1) -- 均摊复杂度
这里的复杂度本来是O(n),因为resize()方法会触发扩容和缩容操作。但是由于不是每一次操作都触发扩容和缩容操作,所以使用均摊复杂度分析更为合理。
- E deQueue(); O(1) -- 均摊复杂度
- E getFront(); O(1)
- int getSize(); O(1)
- boolean isEmpty(); O(1)
以上所有内容都是通过"慕课网"听"liuyubobobo"的《玩转数据结构》课程后总结