基於數組和鏈表Java底層實現Queue隊列和循環隊列

Queue 隊列


1、什麼是隊列

同 Stack 一樣,Queue隊列也是一種線性結構,底層的實現幾乎完全相同,只是套的“皮”不相同。這種數據結構有自己的獨特的特性
Stack -> 先進後出,後進先出
隊列 -> 先進先出,後進後出
從字面意思也不難理解,隊列隊列就是排隊的含義。

根據隊列的性質我們就可以設計隊列的接口函數。

接口函數程序:

public interface Queue<E> {
    int getSize();
    boolean isEmpty();
    void enqueue(E e);  //出隊
    E dequeue();  //入隊
    E getFront();  //查看隊首元素
}

基於Queue接口函數我們就可以實現不同底層的隊列結構,這本文中主要涉及動態數組、鏈表來實現隊列結構。最後根據動態數組的實現改寫,實現循環隊列數據結構。

2、Queue數據結構實現——基於動態數組

主要基於動態數組進行封裝,具體動態數組包含什麼函數,打開可以參考動態數組這一篇文章。

2.1、基本函數實現

@Override
public int getSize() {   //獲取隊列數據大小
    return array.getSize();
}

public int getCapacity() {   //獲取隊列實際容量
    return array.getCapacity();
}

@Override
public boolean isEmpty() {  // 判斷隊列是否爲空
    return array.isEmpty();
}

2.2 進出隊列函數

對於底層爲動態鏈表的隊列,根據時間複雜度分析我們知道,在數組後面增加元素的時間複雜度最低O(1)級別。但是對於進出隊列必須同時對頭尾進行操作,所以無論以數組頭作爲隊首還是以數組尾作爲隊首進出隊列綜合時間複雜度相同。

/**     版本一   **/
@Override
public void enqueue(E e) {
    array.addLast(e);
}

@Override
public E dequeue() {
    return array.removeFirst();
}
/**     版本二   **/
@Override
public void enqueue(E e) {
    array.addFirst(e);
}

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

寫在後面: 導致進出隊列時間複雜度變高是由於向數組頭添加或者刪除元素。改進的思路就是製作循環隊列,也就沒有了刪除或者增加元素導致的數組拷貝操作。後面循環隊列會詳細講解。

2.3 查詢操作

查詢操作就是查看隊首的元素。

@Override
public E getFront() {
    if (isEmpty())
        throw new IllegalArgumentException("Cannot getFront from an empty queue");
    return head.e;
}

3、Queue數據結構實現——基於鏈表函數

和動態數組不同,底層沒有調用自己寫的鏈表類,而是完全從零實現隊列結構。

3.1、基本函數實現

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

@Override
public boolean isEmpty() {
    return size == 0;
}

3.2 進出隊列函數

這裏以鏈表頭最爲隊首,鏈表尾作爲隊尾。我們知道,我們要是訪問隊尾節點需要遍歷整個鏈表,這就大大增加了時間複雜度。對於隊列結構,我們主要是對頭尾進行操作,所以我們引入tail標記。用來標記隊尾節點。這樣我們就可以直接訪問到隊尾元素。head代表鏈表頭,tail代表鏈表尾。出隊操作對應鏈表刪除頭結點,入隊操作代表在鏈表尾增加元素。

這裏也可以使用我們之前實現的 LinkedList 鏈表類, 其中的 removeFirst() 和 addLast() 函數。這裏就是提示作者我們實現的方式有很多,不要僅侷限於一種。但是我們也看出來製作封裝鏈表庫有多麼重要,兩個函數就是是實現的隊列這種數據結構。很多數據結構都是可以基於我們實現的類進一步封裝,將變得非常簡單。

@Override
public void enqueue(E e) {
    if (tail == null){
        tail = new Node(e);
        head = tail;               //第一元素
    }else{
        tail.next = new Node(e);  //增加元素導致tail標記向後移動一位
        tail = tail.next;
    }
    size++;                      //維護size
}

@Override
public E dequeue() {
    if (isEmpty())
        throw new IllegalArgumentException("Cannot dequeue from an empty queue");
    Node retNode = head;  //刪除元素導致head標記向後移動一位
    head = head.next;
    retNode.next = null; // 斷開連接,被Java自動回收機制回收
    if (head == null)
        tail = null;
    size--;            // 維護size
    return retNode.e;
}

3.3 查詢操作

查詢操作就變得十分簡單,直接返回隊首節點的元素值。

@Override
public E getFront() {
    if (isEmpty())
        throw new IllegalArgumentException("Cannot getFront from an empty queue");
    return head.e;
}

4、LoopQueue循環隊列的實現

顧名思義,這裏的數組是一個循環數組。這裏的循環並不是真的循環,而且一種狹義的循環。我們遍歷數組末尾的下一元素的時候,自動將索引回到數組頭的位置。其實就是遍歷的時候,自增變爲

index = (index + 1) % size;
這樣也就實現了索引自動循環。
這裏同上面不同的是,出隊操作並不需要數組重新回到索引爲0的位置,而是保持不變。入隊操作就是直接向數組後面添加元素,如果到達末尾,直接轉向頭進行添加。這裏我們引入head和tail標記。
入隊操作步驟:
向數組尾添加元素,索引位置爲

tail = (tail + 1) % size

我們並不關注圖中標記爲 藍色的節點 。因爲我們實際的元素就是 head -> tail 其他的是啥我們並不關注。

入隊操作

出隊操作步驟:
向數組頭添加元素,索引位置爲

head = (head - 1) % size
出隊操作

寫在前面: 我們不難注意到,數組滿的時候正是head與tail相鄰的時候,tail在前。爲了維護循環性,我們表示爲

tail == (head + 1) % size

4.1、基本函數實現

public int getCapacity() {  //判斷容器是否爲空
    return data.length - 1;
}

@Override
public int getSize() {  //獲取隊列實際數據含量
    return size;
}

@Override
public boolean isEmpty() {  //當頭和尾重合即爲空
    return head == tail;
}

4.2、進出隊列函數

在前面我們已經寫了,最主要的就是索引的維護。實現循環的操作,也就是取餘操作。

@Override
public void enqueue(E e) {
    if ((tail + 1) % data.length == head)  // 判斷數組中是否已經存滿
        resize(getCapacity() * 2);

    data[tail] = e;
    tail = (tail + 1) % data.length;  //tail向後自增
    size++;  // 維護size
}
@Override
public E dequeue() {
    if (isEmpty()) {
        throw new IllegalArgumentException("Cannot dequeue from an empty queue");
    }
    E temp = data[head];
    data[head] = null;
    head = (head + 1) % data.length; //頭節點向後移動一位
    size--;  // 維護size
    if (size == getCapacity() / 4 && getCapacity() / 2 != 0) {  // 防止時間複雜度震盪並保持整除
        resize(getCapacity() / 2);
    }
    return temp;
}

對於resize函數我們就是將原來的數組拷貝至比原數組大以一倍的數組中。遍歷方式就是從 head 遍歷到 tail 。爲了維護安全性,我們將resize函數設置爲私有。

private void resize(int newCapacity) {
    E[] newData = (E[]) new Object[newCapacity + 1];
    for (int i = 0; i < size; i++)
        newData[i] = data[(i + head) % data.length];
    head = 0;
    tail = size;
    data = newData;
}

4.3、查詢操作

查詢操作沒有什麼特別的直接返回head節點的元素。

@Override
public E getFront() {
    if (isEmpty()) {
        throw new IllegalArgumentException("Cannot getFront from an empty queue");
    }
    return data[head];
}

5、時間複雜度分析

入隊操作 出隊操作 查詢操作
數組隊列 O(1) O(N) O(1)
鏈表隊列 O(1) O(1) O(1)
循環隊列 O(1) O(1) O(1)

實際上,循環隊列是對數組隊列的一個優化,減小了數組隊列中在頭部進行入隊或者出隊造成的複雜度。

最後

更多精彩內容,大家可以轉到我的主頁:曲怪曲怪的主頁

或者關注我的微信公衆號:TeaUrn

源碼地址:可在公衆號內回覆 數據結構與算法源碼 即可獲得。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章