数据结构【四】-队列2【循环队列】

一。背景

如果我们想删除队首的元素,这个时候,如果是上面提到的[数组队列],就会变成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"的《玩转数据结构》课程后总结

猜你喜欢

转载自blog.csdn.net/sunshine77_/article/details/87925810
今日推荐