循環隊列 是一種線性數據結構,其操作表現基於 FIFO
(先進先出)原則並且隊尾被連接在隊首之後以形成一個循環。它也被稱爲 環形緩衝器。
循環隊列的一個好處是我們可以利用這個隊列之前用過的空間。在一個普通隊列裏,一旦一個隊列滿了,我們就不能插入下一個元素,即使在隊列前面仍有空間。但是使用循環隊列,我們能使用這些空間去存儲新的值。
爲什麼使用循環隊列?
這裏我們先對 隊列 的簡單實現進行簡單展示,隊列應支持兩種操作:入隊 和 出隊 。
class MyQueue {
// store elements
private List<Integer> data;
// a pointer to indicate the start position
private int p_start;
public MyQueue() {
data = new ArrayList<Integer>();
p_start = 0;
}
/** Insert an element into the queue. Return true if the operation is successful. */
public boolean enQueue(int x) {
data.add(x);
return true;
};
/** Delete an element from the queue. Return true if the operation is successful. */
public boolean deQueue() {
if (isEmpty() == true) {
return false;
}
p_start++;
return true;
}
/** Get the front item from the queue. */
public int Front() {
return data.get(p_start);
}
/** Checks whether the queue is empty or not. */
public boolean isEmpty() {
return p_start >= data.size();
}
}
缺點
上面的實現很簡單,但在某些情況下效率很低。 隨着起始指針的移動,浪費了越來越多的空間。 當我們有空間限制時,這將是難以接受的。
讓我們考慮一種情況,即我們只能分配一個最大長度爲 5 的數組。當我們只添加少於 5 個元素時,我們的解決方案很有效。 例如,如果我們只調用入隊函數四次後還想要將元素 10 入隊,那麼我們可以成功。
但是我們不能接受更多的入隊請求,這是合理的,因爲現在隊列已經滿了。但是如果我們將一個元素出隊呢?
實際上,在這種情況下,我們應該能夠再接受一個元素。
循環隊列 的需求迫在眉睫。
設計思想
這裏直接複製了 liweiwei1419 精彩的 題解 :
1、定義循環變量 front 和 rear 。一直保持這個定義,到底是先賦值還是先移動指針就很容易想清楚了。
front
:指向隊列頭部第 1
個有效數據的位置;
rear
:指向隊列尾部(即最後 1
個有效數據)的下一個位置,即下一個從隊尾入隊元素的位置。
(說明:這個定義是依據“動態數組”的定義模仿而來。)
2、爲了避免“隊列爲空”和“隊列爲滿”的判別條件衝突,我們有意浪費了一個位置。
浪費一個位置是指:循環數組中任何時刻一定至少有一個位置不存放有效元素。
判別隊列爲空的條件是:front == rear
;
判別隊列爲滿的條件是:(rear + 1) % capacity == front
;。可以這樣理解,當 rear
循環到數組的前面,要從後面追上 front
,還差一格的時候,判定隊列爲滿。
3、因爲有循環的出現,要特別注意處理數組下標可能越界的情況。指針後移的時候,索引 + 1,並且要注意取模。
例題
本文的寫作目的就是爲了 再次手寫一邊算法練手。
622.設計循環隊列
- 難度:
Medium
參考上述設計思路,很容易編寫代碼,需要注意的是取模代碼的編寫相對容易出問題。
實現代碼:
class MyCircularQueue {
private int front;
private int tail;
private int capacity;
private int[] arr;
public MyCircularQueue(int k) {
this.capacity = k + 1;
this.arr = new int[capacity];
this.front = 0;
this.tail = 0;
}
public boolean enQueue(int value) {
if (isFull()) return false;
arr[tail] = value;
tail = (tail + 1) % capacity;
return true;
}
public boolean deQueue() {
if (isEmpty()) return false;
front = (front + 1) % capacity;
return true;
}
public int Front() {
if (isEmpty()) return -1;
return arr[front];
}
public int Rear() {
if (isEmpty()) return -1;
return arr[(tail - 1 + capacity) % capacity];
}
public boolean isEmpty() {
return front == tail;
}
public boolean isFull() {
return (tail + 1) % capacity == front;
}
}
641. 設計循環雙端隊列
- 難度:
Medium
和上題基本相似,除了多幾個取模運算,幾乎沒什麼變化。
class MyCircularDeque {
private int front;
private int tail;
private int capacity;
private int[] arr;
public MyCircularDeque(int k) {
this.capacity = k + 1;
this.arr = new int[capacity];
this.front = 0;
this.tail = 0;
}
public boolean insertFront(int value) {
if (isFull()) return false;
front = (front - 1 + capacity) % capacity;
arr[front] = value;
return true;
}
public boolean insertLast(int value) {
if (isFull()) return false;
arr[tail] = value;
tail = (tail + 1) % capacity;
return true;
}
public boolean deleteFront() {
if (isEmpty()) return false;
front = (front + 1) % capacity;
return true;
}
public boolean deleteLast() {
if (isEmpty()) return false;
tail = (tail - 1 + capacity) % capacity;
return true;
}
public int getFront() {
if (isEmpty()) return -1;
return arr[front];
}
public int getRear() {
if (isEmpty()) return -1;
return arr[(tail - 1 + capacity) % capacity];
}
public boolean isEmpty() {
return front == tail;
}
public boolean isFull() {
return (tail + 1) % capacity == front;
}
}
參考 & 感謝
文章絕大部分內容節選自LeetCode
:
https://leetcode-cn.com/problems/design-circular-queue
https://leetcode-cn.com/problems/design-circular-deque
感謝 liweiwei1419 提供的精彩的 題解 。
關於我
Hello,我是 卻把清梅嗅 ,如果您覺得文章對您有價值,歡迎 ❤️,也歡迎關注我的 博客 或者 GitHub。
如果您覺得文章還差了那麼點東西,也請通過關注督促我寫出更好的文章——萬一哪天我進步了呢?