Java版-數據結構-隊列(循環隊列)

前情回顧

在上一篇,筆者給你們介紹了數組隊列,而且在文末提出了數組隊列實現上的劣勢,以及帶來的性能問題(由於數組隊列,在出隊的時候,咱們每每要將數組中的元素往前挪動一個位置,這個動做的時間複雜度O(n)級別),若是不清楚的小夥伴歡迎查看閱讀。爲了方便你們查閱,筆者在這裏貼出相關的地址:java

爲了解決數組隊列帶來的問題,本篇給你們介紹一下循環隊列git

思路分析圖解

囉嗦一下,因爲筆者不太會弄貼出來的圖片帶有動畫效果,好比元素的移動或者刪除(畢竟這樣看你們比較直觀),筆者在這裏只能經過靜態圖片的方式,幫助你們理解實現原理,但願你們不要見怪,若是有朋友知道如何搞的話,歡迎在評論區慧言。github

在這裏,咱們聲明瞭一個容量大小爲8的數組,並標出了索引0-7,而後使用fronttail分別來表示隊列的,隊首隊尾;在下圖中,fronttail的位置一開始都指向是了索引0的位置,這意味着當front == tai的時候 隊列爲空 你們務必牢記這一點,以便區分後面介紹隊列快滿時的臨界條件數組

image-20190321003646859

爲了你們更好地理解下面的內容,在這裏,我簡單作幾點說明數據結構

  • front:表示隊列隊首,始終指向隊列中的第一個元素(當隊列空時,front指向索引爲0的位置)ide

  • tail:表示隊列隊尾,始終指向隊列中的最後一個元素的下一個位置oop

  • 元素入隊,維護tail的位置,進行tail++操做性能

  • 元素出隊,維護front的位置,進行front++操做測試

上面所說的,元素進行入隊和出隊操做,都簡單的進行++操做,來維護tailfront的位置,實際上是不嚴謹的,正確的維護tail的位置應該是(tail + 1) % capacity,同理front的位置應該是(front + 1) % capacity,這也是爲何叫作循環隊列的緣由,你們先在這裏知道下,暫時不理解也不要緊,後面相信你們會知曉。動畫

下面咱們看一下,如今若是有一個元素a入隊,如今的示意圖:

image-20190321011926946

咱們如今看到了元素a入隊,咱們的tail指向的位置發生了變化,進行了++操做,而front的位置,沒有發生改變,仍舊指向索引爲0的位置,還記得筆者上面所說的,front的位置,始終指向隊列中的第一個元素,tail的位置,始終指向隊列中的最後一個元素的下一個位置

如今,咱們再來幾個元素b、c、d、e進行入隊操做,看一下此時的示意圖:

image-20190321012501661

想必你們都能知曉示意圖是這樣,好像沒什麼太多的變化(還請你們彆着急,筆者這也是方便你們理解究竟是什麼循環隊列,還請你們原諒我O(∩_∩)O哈!)

看完了元素的入隊的操做狀況,那如今咱們看一下,元素的出隊操做是什麼樣的?

元素a出隊,示意圖以下:

image-20190321012912749

如今元素a已經出隊,front的位置指向了索引爲1的位置,如今數組中全部的元素再也不須要往前挪動一個位置

這一點和咱們的數組隊列(咱們的數組隊列須要元素出隊,後面的元素都要往前挪動一個位置)徹底不一樣,咱們只須要改變一下front的指向就能夠了,由以前的O(n)操做,變成了O(1)的操做

咱們再次進行元素b出隊,示意圖以下:

image-20190321014047456

到這裏,可能有的小夥伴會問,爲何叫作,循環隊列?那麼如今咱們嘗試一下,咱們讓元素f、g分別進行入隊操做,此時的示意圖以下:

image-20190321015054219

你們目測看下來仍是沒什麼變化,若是此時,咱們再讓一個元素h元素進行入隊操做,那麼問題來了咱們的tail的位置該如何指向呢?示意圖以下:

image-20190321020004501
根據咱們以前說的,元素入隊:維護 tail的位置,進行 tail++操做,而此時咱們的 tail已經指向了索引爲 7的位置,若是咱們此時對 tail進行 ++操做,顯然不可能(數組越界)

細心的小夥伴,會發現此時咱們的隊列並無滿,還剩兩個位置(這是由於咱們元素出隊後,當前的空間,沒有被後面的元素擠掉),你們能夠把咱們的數組想象成一個環狀,那麼索引7以後的位置就是索引0

如何才能從索引7的位置計算到索引0的位置,以前咱們一直說進行tail++操做,筆者也在開頭指出了,這是不嚴謹的,應該的是(tail + 1) % capacity這樣就變成了(7 + 1) % 8等於 0

因此此時若是讓元素h入隊,那麼咱們的tail就指向了索引爲0的位置,示意圖以下:

image-20190323120553239

假設如今又有新的元素k入隊了,那麼tail的位置等於(tail + 1) % capacity 也就是(0 + 1)% 8等於1就指向了索引爲1的位置

image-20190323123428055

那麼問題來了,咱們的循環隊列還能不能在進行元素入隊呢?咱們來分析一下,從圖中顯示,咱們還有一個索引爲0的空的空間位置,也就是此時tail指向的位置

按照以前的邏輯,假設如今能放入一個新元素,咱們的tail進行(tail +1) % capacity計算結果爲2(若是元素成功入隊,此時隊列已經滿了),此時咱們會發現表示隊首的front也指向了索引爲2的位置

若是新元素成功入隊的話,咱們的tail也等於2,那麼此時就成了 tail == front ,一開始咱們提到過,當隊列爲空的tail == front,如今呢,若是隊列爲滿時tail也等於front,那麼咱們就沒法區分,隊列爲滿時和隊列爲空時收的狀況了

因此,在循環隊列中,咱們老是浪費一個空間,來區分隊列爲滿時和隊列爲空時的狀況,也就是當 ( tail + 1 ) % capacity == front的時候,表示隊列已經滿了,當front == tail的時候,表示隊列爲空。

image-20190323124927705

瞭解了循環隊列的實現原理以後,下面咱們用代碼實現一下。

代碼實現

接口定義Queue

public interface Queue<E> {
    /** * 入隊 * * @param e */
    void enqueue(E e);

    /** * 出隊 * * @return */
    E dequeue();

    /** * 獲取隊首元素 * * @return */
    E getFront();

    /** * 獲取隊列中元素的個數 * * @return */
    int getSize();

    /** * 判斷隊列是否爲空 * * @return */
    boolean isEmpty();
}
複製代碼

接口實現:LoopQueue

public class LoopQueue<E> implements Queue<E> {
    /** * 承載隊列元素的數組 */
    private E[] data;
    /** * 隊首的位置 */
    private int front;
    /** * 隊尾的位置 */
    private int tail;
    /** * 隊列中元素的個數 */
    private int size;

    /** * 指定容量,初始化隊列大小 * (因爲循環隊列須要浪費一個空間,因此咱們初始化隊列的時候,要將用戶傳入的容量加1) * * @param capacity */
    public LoopQueue(int capacity) {
        data = (E[]) new Object[capacity + 1];
    }

    /** * 模式容量,初始化隊列大小 */
    public LoopQueue() {
        this(10);
    }


    @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("隊列爲空");
        }
        // 出隊元素
        E element = data[front];
        // 元素出隊後,將空間置爲null
        data[front] = null;
        // 維護front的索引位置(循環隊列)
        front = (front + 1) % data.length;
        // 維護size大小
        size--;

        // 元素出隊後,能夠指定條件,進行縮容
        if (size == getCapacity() / 2 && getCapacity() / 2 != 0) {
            resize(getCapacity() / 2);
        }
        return element;
    }

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


    // 隊列快滿時,隊列擴容;元素出隊操做,指定條件能夠進行縮容
    private void resize(int newCapacity) {
        // 這裏的加1仍是由於循環隊列咱們在實際使用的過程當中要浪費一個空間
        E[] newData = (E[]) new Object[newCapacity + 1];
        for (int i = 0; i < size; i++) {
            // 注意這裏的寫法:由於在數組中,front 可能不是在索引爲0的位置,相對於i有一個偏移量
            newData[i] = data[(i + front) % data.length];
        }
        // 將新的數組引用賦予原數組的指向
        data = newData;
        // 充值front的位置(front老是指向隊列中第一個元素)
        front = 0;
        // size 的大小不變,由於在這過程當中,沒有元素入隊和出隊
        tail = size;
    }


    private int getCapacity() {
        // 注意:在初始化隊列的時候,咱們有意識的爲隊列加了一個空間,那麼它的實際容量天然要減1
        return data.length - 1;
    }

    @Override
    public String toString() {
        return "LoopQueue{" +
                "【隊首】data=" + Arrays.toString(data) + "【隊尾】" +
                ", front=" + front +
                ", tail=" + tail +
                ", size=" + size +
                ", capacity=" + getCapacity() +
                '}';
    }
}
複製代碼

測試類:LoopQueueTest

public class LoopQueueTest {
    @Test
    public void testLoopQueue() {
        LoopQueue<Integer> loopQueue = new LoopQueue<>();
        for (int i = 0; i < 10; i++) {
            loopQueue.enqueue(i);
        }
        // 初始化隊列數據
        System.out.println("原始隊列: " + loopQueue);
        // 元素0出隊
        loopQueue.dequeue();
        System.out.println("元素0出隊: " + loopQueue);
        loopQueue.dequeue();
        System.out.println("元素1出隊: " + loopQueue);
        loopQueue.dequeue();
        System.out.println("元素2出隊: " + loopQueue);
        loopQueue.dequeue();
        System.out.println("元素3出隊: " + loopQueue);
        loopQueue.dequeue();
        System.out.println("元素4出隊,發生縮容: " + loopQueue);
        // 隊首元素
        System.out.println("隊首元素:" + loopQueue.getFront());
    }
}
複製代碼
測試結果:
原始隊列: LoopQueue{【隊首】data=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, null]【隊尾】, front=0, tail=10, size=10, capacity=10}
元素0出隊: LoopQueue{【隊首】data=[null, 1, 2, 3, 4, 5, 6, 7, 8, 9, null]【隊尾】, front=1, tail=10, size=9, capacity=10}
元素1出隊: LoopQueue{【隊首】data=[null, null, 2, 3, 4, 5, 6, 7, 8, 9, null]【隊尾】, front=2, tail=10, size=8, capacity=10}
元素2出隊: LoopQueue{【隊首】data=[null, null, null, 3, 4, 5, 6, 7, 8, 9, null]【隊尾】, front=3, tail=10, size=7, capacity=10}
元素3出隊: LoopQueue{【隊首】data=[null, null, null, null, 4, 5, 6, 7, 8, 9, null]【隊尾】, front=4, tail=10, size=6, capacity=10}
元素4出隊,發生縮容: LoopQueue{【隊首】data=[5, 6, 7, 8, 9, null]【隊尾】, front=0, tail=5, size=5, capacity=5}
隊首元素:5
複製代碼

完整版代碼GitHub倉庫地址:Java版數據結構-隊列(循環隊列) 歡迎你們【關注】和【Star

至此筆者已經爲你們帶來了數據結構:靜態數組、動態數組、棧、數組隊列、循環隊列;接下來,筆者還會一一的實現其它常見的數組結構,你們一塊兒加油。

  • 靜態數組
  • 動態數組
  • 數組隊列
  • 循環隊列
  • 鏈表
  • 循環鏈表
  • 二分搜索樹
  • 優先隊列
  • 線段樹
  • 字典樹
  • AVL
  • 紅黑樹
  • 哈希表
  • ....

持續更新中,歡迎你們關注公衆號:小白程序之路(whiteontheroad),第一時間獲取最新信息!!!

小白程序之路

筆者博客地址:http:www.gulj.cn

相關文章
相關標籤/搜索