数据结构之队列


简介

队列是是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。

队列是一种先进先出的线性表,简称FIFO允许插入的以端称为队尾,允许删除的一端被称为队头。

入队

出队

数组实现队列

public class Array<E> {

    private E[] data;
    private int size;

    // 构造函数,传入数组的容量capacity构造Array
    public Array(int capacity){
        data = (E[])new Object[capacity];
        size = 0;
    }

    // 无参数的构造函数,默认数组的容量capacity=10
    public Array(){
        this(10);
    }

    // 获取数组的容量
    public int getCapacity(){
        return data.length;
    }

    // 获取数组中的元素个数
    public int getSize(){
        return size;
    }

    // 返回数组是否为空
    public boolean isEmpty(){
        return size == 0;
    }

    // 在index索引的位置插入一个新元素e
    public void add(int index, E e){

        if(index < 0 || index > size)
            throw new IllegalArgumentException("Add failed. Require index >= 0 and index <= size.");

        if(size == data.length)
            resize(2 * data.length);

        for(int i = size - 1; i >= index ; i --)
            data[i + 1] = data[i];

        data[index] = e;

        size ++;
    }

    // 向所有元素后添加一个新元素
    public void addLast(E e){
        add(size, e);
    }

    // 在所有元素前添加一个新元素
    public void addFirst(E e){
        add(0, e);
    }

    // 获取index索引位置的元素
    public E get(int index){
        if(index < 0 || index >= size)
            throw new IllegalArgumentException("Get failed. Index is illegal.");
        return data[index];
    }

    public E getLast(){
        return get(size - 1);
    }

    public E getFirst(){
        return get(0);
    }

    // 修改index索引位置的元素为e
    public void set(int index, E e){
        if(index < 0 || index >= size)
            throw new IllegalArgumentException("Set failed. Index is illegal.");
        data[index] = e;
    }

    // 查找数组中是否有元素e
    public boolean contains(E e){
        for(int i = 0 ; i < size ; i ++){
            if(data[i].equals(e))
                return true;
        }
        return false;
    }

    // 查找数组中元素e所在的索引,如果不存在元素e,则返回-1
    public int find(E e){
        for(int i = 0 ; i < size ; i ++){
            if(data[i].equals(e))
                return i;
        }
        return -1;
    }

    // 从数组中删除index位置的元素, 返回删除的元素
    public E remove(int index){
        if(index < 0 || index >= size)
            throw new IllegalArgumentException("Remove failed. Index is illegal.");

        E ret = data[index];
        for(int i = index + 1 ; i < size ; i ++)
            data[i - 1] = data[i];
        size --;
        data[size] = null; // loitering objects != memory leak

        if(size == data.length / 4 && data.length / 2 != 0)
            resize(data.length / 2);
        return ret;
    }

    // 从数组中删除第一个元素, 返回删除的元素
    public E removeFirst(){
        return remove(0);
    }

    // 从数组中删除最后一个元素, 返回删除的元素
    public E removeLast(){
        return remove(size - 1);
    }

    // 从数组中删除元素e
    public void removeElement(E e){
        int index = find(e);
        if(index != -1)
            remove(index);
    }

    @Override
    public String toString(){

        StringBuilder res = new StringBuilder();
        res.append(String.format("Array: size = %d , capacity = %d\n", size, data.length));
        res.append('[');
        for(int i = 0 ; i < size ; i ++){
            res.append(data[i]);
            if(i != size - 1)
                res.append(", ");
        }
        res.append(']');
        return res.toString();
    }

    // 将数组空间的容量变成newCapacity大小
    private void resize(int newCapacity){

        E[] newData = (E[])new Object[newCapacity];
        for(int i = 0 ; i < size ; i ++)
            newData[i] = data[i];
        data = newData;
    }
}
public interface Queue<E> {
    int getSize();
    boolean isEmpty();
    void enqueue(E e);
    E dequeue();
    E getFront();
}
public class ArrayQueue<E> implements Queue<E> {
    private Array<E> array;

    public ArrayQueue(int capacity) {
        array = new Array<>(capacity);
    }

    public ArrayQueue() {
        array = new Array<>();
    }

    @Override
    public int getSize() {
        return array.getSize();
    }

    @Override
    public boolean isEmpty() {
        return array.isEmpty();
    }

    public int getCapacity() {
        return array.getCapacity();
    }

    @Override
    public void enqueue(E e) {
        array.addLast(e);
    }

    @Override
    public E dequeue() {
        return array.removeFirst();
    }

    @Override
    public E getFront() {
        return array.getFirst();
    }

    @Override
    public String toString() {

        StringBuilder res = new StringBuilder();
        res.append("Queue:  ");
        res.append("front [ ");
        for (int i = 0; i < array.getSize(); i++) {
            res.append(array.get(i));
            if (i != array.getSize() - 1)
                res.append(", ");
        }
        res.append("] tail");

        return res.toString();
    }

    public static void main(String[] args) {
        ArrayQueue<Integer> queue = new ArrayQueue <> ();
        System.out.println();
        for (int i = 0; i < 100000; i++) {
            queue.enqueue(i);
            System.out.println(queue);
            if (i % 3 == 2) {
                queue.dequeue();
                System.out.println(queue);
            }
        }
    }
}

测试

复杂度分析

ArrayQueue<E>
void enqueue(E e) O(1)均摊
E dequeue() O(n)
E getFront o(1)
int getSize O(1)
boolean isEmpty() O(1)

如果使用顺序队列的话,试想一下如果我们队列十分的大,那么我们需要耗费的时间是多少呢。

循环队列

队列首位相接的顺序存储结构。

通过这样的方法,我们成功避免了数据搬移操作。看起来不难理解在用数组实现的非循环队列中,队满的判断条件是 tail == n,队空的判断条件是 head == tail。那针对循环队列,如何判断队空和队满呢?
队列为空的判断条件仍然是 head == tail。但队列满的判断条件就稍微有点复杂了。

tail=3,head=4,n=8,所以总结一下规律就是:(3+1)%8=4。当队满时,(tail+1)%n=head,当队列满时,图中的 tail 指向的位置实际上是没有存储数据的。所以,循环队列会 浪费一个数组的存储空间。

基于数组实现

循环队列需要我们单独去实现。

public interface Queue<E> {
    int getSize();
    boolean isEmpty();
    void enqueue(E e);
    E dequeue();
    E getFront();
}
public class LoopQueue<E> implements Queue<E> {

   private E[] data;
   private int front, tail;
   private int size;  // 有兴趣的同学,在完成这一章后,可以思考一下:
   // LoopQueue中不声明size,如何完成所有的逻辑?
   // 这个问题可能会比大家想象的要难一点点:)

   public LoopQueue(int capacity){
       data = (E[])new Object[capacity + 1];
       front = 0;
       tail = 0;
       size = 0;
   }

   public LoopQueue(){
       this(10);
   }

   public int getCapacity(){
       return data.length - 1;
   }

   @Override
   public boolean isEmpty(){
       return front == tail;
   }

   @Override
   public int getSize(){
       return size;
   }

   @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;
   }

   @Override
   public E getFront(){
       if(isEmpty())
           throw new IllegalArgumentException("Queue is empty.");
       return data[front];
   }

   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;
   }

   @Override
   public String toString(){

       StringBuilder res = new StringBuilder();
       res.append(String.format("Queue: size = %d , capacity = %d\n", size, getCapacity()));
       res.append("front [");
       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();
   }

   public static void main(String[] args){   //测试

       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);
           }
       }
   }
}

对比

import java.util.Random;

public class Main {

    // 测试使用q运行opCount个enqueueu和dequeue操作所需要的时间,单位:秒
    private static double testQueue(Queue<Integer> q, int opCount) {

        long startTime = System.nanoTime();

        Random random = new Random();
        for (int i = 0; i < opCount; i++) {
            q.enqueue(random.nextInt(Integer.MAX_VALUE));
        }
        for (int i = 0; i < opCount; i++) {
            q.dequeue();
        }

        long endTime = System.nanoTime();

        return (endTime - startTime) / 1000000000.0;  //纳秒10^9
    }

    public static void main(String[] args) {

        int opCount = 100000;    //随机10万个数插入队列

        ArrayQueue<Integer> arrayQueue = new ArrayQueue<>();
        double time1 = testQueue(arrayQueue, opCount);
        System.out.println("ArrayQueue, time: " + time1 + " s");

        LoopQueue<Integer> loopQueue = new LoopQueue<>();
        double time2 = testQueue(loopQueue, opCount);
        System.out.println("LoopQueue, time: " + time2 + " s");
    }
}

可见时间差距是将近200多倍。

应用

阻塞队列

阻塞队列在队列基础上增加了阻塞操作。在队列为空的时候,从队头取数据会被阻塞。因为此时还没有数据可取,直到队列中有了数据才能返回;如果队列已经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后再返回。

基于阻塞队列实现的“生产者 - 消费者模型”,可以有效地协调生产和消费的速度。当“生产者”生 产数据的速度过快,“消费者”来不及消费时,存储数据的队列很快就会满了。这个时候,生产者就阻塞等待,直到“消费者”消费了数据,“生产者”才会被唤醒继续“生产”。
而且不仅如此,基于阻塞队列,我可以通过协调“生产者”和“消费者”的个数,来提高数据的处理效率。比如前面的例子,我们可以多配置几个“消费者”,来应对一个“生产者”。

并发队列

线程安全的队列我们叫作并发队列。最简单直接的实现方式是直接在 enqueue()、dequeue() 方法 上加锁,但是锁粒度大并发度会比较低,同一时刻仅允许一个存或者取操作。实际上,基于数组的循环队列,利用 CAS 原子操作,可以实现非常高效的并发队列。这也是循环队列比链式队列应用 更加广泛的原因。

参考资料

《数据结构与算法之美》

《玩转数据结构》

《大话数据结构》


文章作者: 清风笑丶
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 清风笑丶 !
 上一篇
数据结构之链表 数据结构之链表
链表数组要一块连续的内存空间来存储,对内存的要求比较高。如果我们申请一个100MB大小的数组,当内存中没有连续的、足够大的存储空间 时,即便内存的剩余总可用空间大于 100MB,仍然会申请失败。链表恰恰相反,它并不需要一块连续的内存空间,它
2019-05-04
下一篇 
数据结构之栈 数据结构之栈
简介限定仅在表尾进行插入和删除操作的线性表。允许插入和删除的一端成为栈顶,另一端成为栈低,不含任何元素的栈成为空栈,栈又称为先进先出的线性表,简称LIFO结构。 栈的插入操作,叫做进栈,也称压栈,入栈。 栈的删除操作,也叫出战,也有的叫做弹
2019-05-03
  目录