前情回顧
在上一篇,筆者給大家介紹了陣列佇列
,並且在文末提出了陣列佇列
實現上的劣勢,以及帶來的效能問題(因為陣列佇列,在出隊的時候,我們往往要將陣列中的元素往前挪動一個位置,這個動作的時間複雜度O(n)級別),如果不清楚的小夥伴歡迎檢視閱讀。為了方便大家查閱,筆者在這裡貼出相關的地址:
為了解決陣列佇列
帶來的問題,本篇給大家介紹一下迴圈佇列
。
思路分析圖解
囉嗦一下,由於筆者不太會弄貼出來的圖片帶有動畫效果,比如元素的移動或者刪除(畢竟這樣看大家比較直觀),筆者在這裡只能通過靜態圖片的方式,幫助大家理解實現原理,希望大家不要見怪,如果有朋友知道如何搞的話,歡迎在評論區慧言。
在這裡,我們宣告瞭一個容量大小為8
的陣列,並標出了索引0-7
,然後使用front
和tail
分別來表示佇列的,隊首和隊尾;在下圖中,front
和tail
的位置一開始都指向是了索引0
的位置,這意味著當front == tai
的時候 佇列為空 大家務必牢記這一點,以便區分後面介紹佇列快滿時的臨界條件
為了大家更好地理解下面的內容,在這裡,我簡單做幾點說明
-
front
:表示佇列隊首,始終指向佇列中的第一個元素(當佇列空時,front
指向索引為0的位置) -
tail
:表示佇列隊尾,始終指向佇列中的最後一個元素的下一個位置 -
元素入隊,維護
tail
的位置,進行tail++
操作 -
元素出隊,維護
front
的位置,進行front++
操作
上面所說的,元素進行入隊和出隊操作,都簡單的進行
++
操作,來維護tail
和front
的位置,其實是不嚴謹的,正確的維護tail
的位置應該是(tail + 1) % capacity
,同理front
的位置應該是(front + 1) % capacity
,這也是為什麼叫做迴圈佇列的原因,大家先在這裡知道下,暫時不理解也沒關係,後面相信大家會知曉。
下面我們看一下,現在如果有一個元素a
入隊,現在的示意圖:
我們現在看到了元素
a
入隊,我們的tail
指向的位置發生了變化,進行了++
操作,而front
的位置,沒有發生改變,仍舊指向索引為0
的位置,還記得筆者上面所說的,front
的位置,始終指向佇列中的第一個元素,tail
的位置,始終指向佇列中的最後一個元素的下一個位置
現在,我們再來幾個元素b、c、d、e
進行入隊操作,看一下此時的示意圖:
想必大家都能知曉示意圖是這樣,好像沒什麼太多的變化(還請大家彆著急,筆者這也是方便大家理解到底是什麼迴圈佇列,還請大家原諒我O(∩_∩)O哈!)
看完了元素的入隊的操作情況,那現在我們看一下,元素的出隊操作是什麼樣的?
元素a
出隊,示意圖如下:
現在元素a
已經出隊,front
的位置指向了索引為1
的位置,現在陣列中所有的元素不再需要往前挪動一個位置
這一點和我們的陣列佇列(我們的陣列佇列需要元素出隊,後面的元素都要往前挪動一個位置)完全不同,我們只需要改變一下front
的指向就可以了,由之前的O(n)操作,變成了O(1)的操作
我們再次進行元素b
出隊,示意圖如下:
到這裡,可能有的小夥伴會問,為什麼叫做,迴圈佇列?那麼現在我們嘗試一下,我們讓元素f、g
分別進行入隊操作,此時的示意圖如下:
大家目測看下來還是沒什麼變化,如果此時,我們再讓一個元素h
元素進行入隊操作,那麼問題來了
我們的tail
的位置該如何指向呢?示意圖如下:
tail
的位置,進行tail++
操作,而此時我們的tail
已經指向了索引為7
的位置,如果我們此時對tail
進行++
操作,顯然不可能(陣列越界)
細心的小夥伴,會發現此時我們的佇列並沒有滿,還剩兩個位置(這是因為我們元素出隊後,當前的空間,沒有被後面的元素擠掉),大家可以把我們的陣列想象成一個環狀,那麼索引7
之後的位置就是索引0
如何才能從索引7
的位置計算到索引0
的位置,之前我們一直說進行tail++
操作,筆者也在開頭指出了,這是不嚴謹的,應該的是(tail + 1) % capacity
這樣就變成了(7 + 1) % 8
等於 0
所以此時如果讓元素h
入隊,那麼我們的tail
就指向了索引為0
的位置,示意圖如下:
假設現在又有新的元素k
入隊了,那麼tail的位置等於(tail + 1) % capacity
也就是(0 + 1)% 8
等於1
就指向了索引為1
的位置
那麼問題來了,我們的迴圈佇列還能不能在進行元素入隊呢?我們來分析一下,從圖中顯示,我們還有一個索引為0
的空的空間位置,也就是此時tail
指向的位置
按照之前的邏輯,假設現在能放入一個新元素,我們的tail
進行(tail +1) % capacity
計算結果為2
(如果元素成功入隊,此時佇列已經滿了),此時我們會發現表示隊首的front
也指向了索引為2
的位置
如果新元素成功入隊的話,我們的tail
也等於2
,那麼此時就成了 tail == front
,一開始我們提到過,當佇列為空的tail == front
,現在呢,如果佇列為滿時tail
也等於front
,那麼我們就無法區分,佇列為滿時和佇列為空時收的情況了
所以,在迴圈佇列中,我們總是浪費一個空間,來區分佇列為滿時和佇列為空時的情況,也就是當 ( tail + 1 ) % capacity == front
的時候,表示佇列已經滿了,當front == tail
的時候,表示佇列為空。
瞭解了迴圈佇列的實現原理之後,下面我們用程式碼實現一下。
程式碼實現
介面定義 :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