在上一篇,筆者給你們介紹了數組隊列
,而且在文末提出了數組隊列
實現上的劣勢,以及帶來的性能問題(由於數組隊列,在出隊的時候,咱們每每要將數組中的元素往前挪動一個位置,這個動做的時間複雜度O(n)級別),若是不清楚的小夥伴歡迎查看閱讀。爲了方便你們查閱,筆者在這裏貼出相關的地址:java
爲了解決數組隊列
帶來的問題,本篇給你們介紹一下循環隊列
。git
囉嗦一下,因爲筆者不太會弄貼出來的圖片帶有動畫效果,好比元素的移動或者刪除(畢竟這樣看你們比較直觀),筆者在這裏只能經過靜態圖片的方式,幫助你們理解實現原理,但願你們不要見怪,若是有朋友知道如何搞的話,歡迎在評論區慧言。github
在這裏,咱們聲明瞭一個容量大小爲8
的數組,並標出了索引0-7
,而後使用front
和tail
分別來表示隊列的,隊首和隊尾;在下圖中,front
和tail
的位置一開始都指向是了索引0
的位置,這意味着當front == tai
的時候 隊列爲空 你們務必牢記這一點,以便區分後面介紹隊列快滿時的臨界條件數組
爲了你們更好地理解下面的內容,在這裏,我簡單作幾點說明數據結構
front
:表示隊列隊首,始終指向隊列中的第一個元素(當隊列空時,front
指向索引爲0的位置)ide
tail
:表示隊列隊尾,始終指向隊列中的最後一個元素的下一個位置oop
元素入隊,維護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】
至此筆者已經爲你們帶來了數據結構:靜態數組、動態數組、棧、數組隊列、循環隊列;接下來,筆者還會一一的實現其它常見的數組結構,你們一塊兒加油。
持續更新中,歡迎你們關注公衆號:小白程序之路(whiteontheroad),第一時間獲取最新信息!!!
筆者博客地址:http:www.gulj.cn