1. 程式人生 > >資料結構——佇列Queue(陣列佇列和迴圈佇列)

資料結構——佇列Queue(陣列佇列和迴圈佇列)

什麼是佇列?

佇列是一種線性的資料結構【線性資料結構:陣列、棧、佇列】

相比陣列,佇列對應的資料操作是陣列的子集。

只能從一端(隊尾)新增元素,只能從另一端(隊首)取出元素。

陣列佇列

程式碼實現

Array陣列類

package cn.itcats.queue;

public class Array<E> {
    private E[] data;
    private int size;

    public Array(int capacity) {
        data =(E[]) new Object[capacity];
        size = 0;
    }

    public Array() {
        //空引數構造預設的capacity為10
        this(10);
    }

    //獲取陣列元素中元素個數
    public int getSize() {
        return this.size;
    }

    //獲得陣列容量
    public int getCapacity() {
        return data.length;
    }

    //判斷陣列是否為空
    public boolean isEmpty() {
        return size == 0;
    }

    //向陣列尾部新增元素
    public void addLast(E e) {
        add(size, e);
    }

    //向陣列頭部新增元素
    public void addFirst(E e) {
        add(0, e);
    }

    //向陣列中index索引處插入某個元素
    public void add(int index, E e) {
        //檢查陣列中是否能容納新的元素
        if (size == data.length)
            System.out.println("陣列需要擴容");
            resize(data.length * 2);
        if (index < 0 || index > size)
            throw new IllegalArgumentException("index非法");
        //移動元素
        for (int i = size - 1; i >= index; i--) {
            //後一個索引賦上前一個索引的元素,即每一個元素都向後挪了一個位置
            data[i + 1] = data[i];
        }
        data[index] = e;
        size++;
    }

    //獲取陣列中的值
    E get(int index) {
        if (index < 0 || index >= size)
            throw new IllegalArgumentException("index非法");
        return data[index];
    }

    //獲取陣列中的值
    E getLast() {
        return get(size - 1);
    }

    //獲取陣列中的值
    E getFirst() {
        return get(0);
    }

    //更新陣列的值
    E update(int index, E e) {
        E oldValue = get(index);
        data[index] = e;
        return oldValue;
    }

    //陣列中是否含有某元素,有返回true,無返回false
    public boolean contains(E e) {
        for (int i = 0; i < size; i++) {
            if (data[i].equals(e))
                return true;
        }
        return false;
    }

    //查詢陣列中的某個元素,找到返回索引,找不到返回-1
    public int find(E e) {
        for (int i = 0; i < size; i++) {
            if (data[i].equals(e)) {
                return i;
            }
        }
        return -1;
    }

    //刪除陣列中某個元素
    public E remove(int index) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("index非法");
        }
        E ret = data[index];
        for (int i = index + 1; i < size; i++) {
            data[i - 1] = data[i];
        }
        size--;
        data[size] = null;
        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);
    }

    //刪除指定元素,如果有則刪除,刪除成功返回true,刪除失敗返回false
    public boolean removeElement(E e) {
        int index = find(e);
        if(index != -1){
            remove(index);
            return true;
        }
        return false;
    }

    //實現動態陣列,動態擴容 size==data.lenngth 擴容2倍 和 縮容 size == data.length / 2
    private void resize(int newCapacity){
        //建立一個新的陣列
        E[] newData = (E[]) new Object[newCapacity];
        //把原來的元素遷移到新的陣列中
        for(int i = 0 ; i < size ; i++){
            newData[i] = data[i];
        }
        data = newData;
    }

    //列印陣列中的元素
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(String.format("Array: size = %d , capacity = %d\n", size, data.length));
        //顯示[1,2,3,4,5]
        sb.append("[");
        for (int i = 0; i < size; i++) {
            sb.append(data[i]);
            if (i != size - 1) {
                sb.append(", ");
            }
        }
        sb.append("]");
        return sb.toString();
    }


}

Queue介面

package cn.itcats.queue;

public interface Queue<E> {
    //入隊操作
    void enqueue(E e);
    //出隊操作
    E dequeue();
    //獲取隊首元素
    E getFront();
    //獲取佇列中元素個數
    int getSize();
    //判斷佇列是否為空
    boolean isEmpty();
}

基於陣列實現的Queue

package cn.itcats.queue;

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 void enqueue(E e) {
        array.addLast(e);
    }

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

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

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

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

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

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(String.format("Queue: "));
        sb.append("front [");
        for (int i = 0; i < array.getSize(); i++) {
            sb.append(array.get(i));
            if (i != array.getSize() - 1) {
                sb.append(", ");
            }
        }
        sb.append("] tail");
        return sb.toString();
    }

    //測試
    public static void main(String[] args) {
        ArrayQueue<Integer> queue = new ArrayQueue<>();
        for(int i = 0 ; i < 10 ;i ++){
            queue.enqueue(i);
            System.out.println("i:"+i +" "+queue);

            if(i % 3 == 2){
                queue.dequeue();
                System.out.println("i:"+i +" "+queue);
            }
        }
    }

}

使用陣列實現佇列的複雜度分析:

可以看到,因為每次出隊都呼叫removeFirst()方法,時間複雜度O(n),那麼有沒有什麼方法可以實現入隊和出隊時間複雜度都是O(1)呢?答案是有的,下面我們就來說說迴圈佇列

迴圈佇列

由於陣列佇列的出隊時間複雜度都為O(n),原因是每次出隊時都需要移動元素進行賦值操作,那麼迴圈佇列便產生了。

迴圈佇列內部維護了兩個指標,front和tail。

1、在初始化時,front和tail都指向索引為0的位置,所以當front==tail時,佇列為空

2、每入隊一個元素,tail++,每出隊一個元素,front++。

3、到tail++到末尾時,若開頭還有可利用的空間,則7之後的索引其實是0。  index = (7+1) % capacitySize == 0

3、如下圖所示,如果在索引1處放置一個元素,tail++,此時tail==front,但此時佇列並不為空,這並不是我們想看見的,所以陣列中需要浪費一個索引空間當(tail+1) % capacity ==front時,佇列為滿,所以索引1不放元素。有些人可能疑問為什麼不是tail+1 == front呢?當tail==7時,再加入一個元素,靠的就是取模運算把下一個tail放在索引0位置的,當tail為0,front為7時,佇列也是滿的,而7+1 != front,不能判斷佇列是滿的,所以需要(tail+1) % capacity ==front,判斷佇列為滿。

程式碼實現:

package cn.itcats.queue;

/**
 * 迴圈佇列   基於陣列
 * @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];
        front = 0;
        tail = 0;
        size = 0;
    }
    public LoopQueue(){
        this(10);
    }

    //入隊操作
    @Override
    public void enqueue(E e) {
        //入隊之前判斷佇列是否為滿
        if(front == (tail+1) % data.length){
            //佇列滿了,擴容
            resize(getCapacity() * 2);
        }
        data[tail] = e;
        tail = (tail + 1) % data.length;
        size++;
    }

    //出隊操作
    @Override
    public E dequeue() {
        //佇列為空
        if(isEmpty())
            throw new IllegalArgumentException("佇列為空,無法出隊");
        //佇列不為空,出隊元素
        E e = data[front];
        //出隊後把元素置空
        data[front] = null;
        front = (front + 1) % data.length;
        size -- ;

        //若出隊到一定數量時,動態縮小容量
        if(size == getCapacity() / 4 && getCapacity() / 2 != 0 ){
            resize(getCapacity() / 2);
        }
        return e;
    }

    //擴容過程
    private void resize(int newCapacity){
        //構建一個新的陣列   長度為原來可用空間的兩倍
        E[] newData = (E[]) new Object[newCapacity + 1];
        //遷移陣列
        for(int i = 0 ; i < size ; i++){
            //把原來front位置的元素依次遷移到新陣列0開始的位置,存在偏移量front
            newData[i] = data[(i+front) % data.length];
        }
        //改變引用
        data = newData;
        //front和tail歸位
        front = 0 ;
        tail = size;
    }

    //檢視隊首元素,但不出隊
    @Override
    public E getFront() {
        //佇列為空
        if(isEmpty())
            throw new IllegalArgumentException("佇列為空,無法檢視隊首元素");
        return data[front];
    }

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

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

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

    //列印陣列中的元素
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(String.format("LoopQueue: size = %d , capacity = %d\n", size, getCapacity()));
        //顯示[1,2,3,4,5]
        sb.append("front [");
        for (int i = front; i != tail; i = (i+1) % data.length) {
            sb.append(data[i]);
            //如果當前索引不是最後一個元素
            if ( (i+1) % data.length != tail) {
                sb.append(", ");
            }
        }
        sb.append("] tail");
        return sb.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("i:"+i +" "+queue);

            if(i % 3 == 2){
                queue.dequeue();
                System.out.println("i:"+i +" "+queue);
            }
        }
    }
}

陣列佇列與迴圈佇列的比較

1、迴圈佇列相比陣列佇列底層都是基於陣列,但迴圈佇列編碼更復雜一些,它引入了front和tail指標。

2、陣列隊列出隊時間複雜度為O(n),而迴圈隊列出隊時間複雜度為O(1)。【均攤複雜度】,因為可能觸發縮容操作。

為此,我們可以對二者進行效能測試。

package cn.itcats.queue;

import java.util.Random;

public class SpeedTest {


    //模擬執行count次入隊和出隊所需時間,單位:  s 秒
    public static double speedTest(Queue<Integer> queue, int count){
        long startTime = System.nanoTime();
        Random random = new Random();
        for(int i = 0 ; i < count ; i++)
            queue.enqueue(random.nextInt(Integer.MAX_VALUE));
        for(int i = 0 ; i < count ; i++)
            queue.dequeue();
        long endTime = System.nanoTime();
        return (endTime - startTime) /1000000000.0 ;
    }

    public static void main(String[] args) {
        //進行入隊和出隊的次數
        int count = 2000000;
        //使用陣列佇列ArrayQueue
        Queue arrayQueue = new ArrayQueue();
        System.out.println("ArrayQueue陣列佇列執行的時間:  "+speedTest(arrayQueue,count)+"  s");
        //使用迴圈佇列LoopQueue
        Queue loopQueue = new LoopQueue();
        System.out.println("LoopQueue迴圈佇列執行的時間:  " + speedTest(loopQueue,count)+"  s");
    }
}

發現loopQueue要比arrayQueue快上百倍。