- 隊列是典型的 FIFO 數據結構。
- 插入(insert)操作也稱作入隊(enqueue),新元素始終被添加在
隊列的末尾
。 - 刪除(delete)操作也被稱爲出隊(dequeue)。 你只能移除
第一個元素
。
隊列的實現
爲了實現隊列,我們可以使用動態數組和指向隊列頭部的索引。
如上所述,隊列應支持兩種操作:入隊和出隊。入隊會向隊列追加一個新元素,而出隊會刪除第一個元素。 所以我們需要一個索引來指出起點。
#include <iostream>
class MyQueue {
private:
// store elements
vector<int> data;
// a pointer to indicate the start position
int p_start;
public:
MyQueue() {p_start = 0;}
/** Insert an element into the queue. Return true if the operation is successful. */
bool enQueue(int x) {
data.push_back(x);
return true;
}
/** Delete an element from the queue. Return true if the operation is successful. */
bool deQueue() {
if (isEmpty()) {
return false;
}
p_start++;
return true;
};
/** Get the front item from the queue. */
int Front() {
return data[p_start];
};
/** Checks whether the queue is empty or not. */
bool isEmpty() {
return p_start >= data.size();
}
};
int main() {
MyQueue q;
q.enQueue(5);
q.enQueue(3);
if (!q.isEmpty()) {
cout << q.Front() << endl;
}
q.deQueue();
if (!q.isEmpty()) {
cout << q.Front() << endl;
}
q.deQueue();
if (!q.isEmpty()) {
cout << q.Front() << endl;
}
}
缺點
上面的實現很簡單,但在某些情況下效率很低。 隨着起始指針的移動,浪費了越來越多的空間。 當我們有空間限制時,這將是難以接受的。
讓我們考慮一種情況,即我們只能分配一個最大長度爲 5 的數組。當我們只添加少於 5 個元素時,我們的解決方案很有效。 例如,如果我們只調用入隊函數四次後還想要將元素 10 入隊,那麼我們可以成功。
但是我們不能接受更多的入隊請求,這是合理的,因爲現在隊列已經滿了。但是如果我們將一個元素出隊呢?
循環隊列
此前,我們提供了一種簡單但低效的隊列實現。
更有效的方法是使用循環隊列。 具體來說,我們可以使用固定大小的數組
和兩個指針
來指示起始位置和結束位置。 目的是重用
我們之前提到的被浪費的存儲
。
設計你的循環隊列實現。 循環隊列是一種線性數據結構,其操作表現基於 FIFO(先進先出)原則並且隊尾被連接在隊首之後以形成一個循環。它也被稱爲“環形緩衝器”。
循環隊列的一個好處是可以利用這個隊列之前用過的空間。在一個普通隊列裏,一旦一個隊列滿了,我們就不能插入下一個元素,即使在隊列前面仍有空間。但是使用循環隊列,我們能使用這些空間去存儲新的值。
你的實現應該支持如下操作:
MyCircularQueue(k)
: 構造器,設置隊列長度爲 k 。Front
: 從隊首獲取元素。如果隊列爲空,返回 -1 。Rear
: 獲取隊尾元素。如果隊列爲空,返回 -1 。enQueue(value)
: 向循環隊列插入一個元素。如果成功插入則返回真。deQueue()
: 從循環隊列中刪除一個元素。如果成功刪除則返回真。isEmpty()
: 檢查循環隊列是否爲空。isFull()
: 檢查循環隊列是否已滿。
class MyCircularQueue {
private:
vector<int> data;
int head;
int tail;
int size;
public:
/** Initialize your data structure here. Set the size of the queue to be k. */
MyCircularQueue(int k) {
data.resize(k);
head = -1;
tail = -1;
size = k;
}
/** Insert an element into the circular queue. Return true if the operation is successful. */
bool enQueue(int value) {
if (isFull()) {
return false;
}
if (isEmpty()) {
head = 0;
}
tail = (tail + 1) % size;
data[tail] = value;
return true;
}
/** Delete an element from the circular queue. Return true if the operation is successful. */
bool deQueue() {
if (isEmpty()) {
return false;
}
if (head == tail) {
head = -1;
tail = -1;
return true;
}
head = (head + 1) % size;
return true;
}
/** Get the front item from the queue. */
int Front() {
if (isEmpty()) {
return -1;
}
return data[head];
}
/** Get the last item from the queue. */
int Rear() {
if (isEmpty()) {
return -1;
}
return data[tail];
}
/** Checks whether the circular queue is empty or not. */
bool isEmpty() {
return head == -1;
}
/** Checks whether the circular queue is full or not. */
bool isFull() {
return ((tail + 1) % size) == head;
}
};
/**
* Your MyCircularQueue object will be instantiated and called as such:
* MyCircularQueue obj = new MyCircularQueue(k);
* bool param_1 = obj.enQueue(value);
* bool param_2 = obj.deQueue();
* int param_3 = obj.Front();
* int param_4 = obj.Rear();
* bool param_5 = obj.isEmpty();
* bool param_6 = obj.isFull();
*/
隊列的內置庫用法
大多數流行語言都提供內置的隊列庫,因此您無需重新發明輪子。
如前所述,隊列有兩個重要的操作,入隊 enqueue
和出隊 dequeue
。 此外,我們應該能夠獲得隊列中的第一個元素
,因爲應該首先處理它。
當你想要按順序處理元素
時,使用隊列可能是一個很好的選擇。
#include <iostream>
int main() {
// 1. Initialize a queue.
queue<int> q;
// 2. Push new element.
q.push(5);
q.push(13);
q.push(8);
q.push(6);
// 3. Check if queue is empty.
if (q.empty()) {
cout << "Queue is empty!" << endl;
return 0;
}
// 4. Pop an element.
q.pop();
// 5. Get the first element.
cout << "The first element is: " << q.front() << endl;
// 6. Get the last element.
cout << "The last element is: " << q.back() << endl;
// 7. Get the size of the queue.
cout << "The size is: " << q.size() << endl;
}
隊列和廣度優先搜索(BFS)
廣度優先搜索(BFS)的一個常見應用是找出從根結點到目標結點的最短路徑。在本文中,我們提供了一個示例來解釋在 BFS 算法中是如何逐步應用隊列的。
示例
這裏我們提供一個示例來說明如何使用 BFS 來找出根結點 A
和目標結點 G
之間的最短路徑。
1. 結點的處理順序是什麼?
在第一輪中,我們處理根結點。在第二輪中,我們處理根結點旁邊的結點;在第三輪中,我們處理距根結點兩步的結點;等等等等。
與樹的層序遍歷類似,越是接近根結點的結點將越早地遍歷
。
如果在第 k 輪中將結點 X
添加到隊列中,則根結點與 X
之間的最短路徑的長度恰好是 k
。也就是說,第一次找到目標結點時,你已經處於最短路徑中。
2. 隊列的入隊和出隊順序是什麼?
如上面的動畫所示,我們首先將根結點排入隊列。然後在每一輪中,我們逐個處理已經在隊列中的結點,並將所有鄰居添加到隊列中。值得注意的是,新添加的節點不會
立即遍歷,而是在下一輪中處理。
結點的處理順序與它們添加
到隊列的順序是完全相同的順序
,即先進先出(FIFO)。這就是我們在 BFS 中使用隊列的原因。
廣度優先搜索 - 模板
使用 BFS 的兩個主要方案:遍歷
或找出最短路徑
。通常,這發生在樹或圖中,BFS 也可以用於更抽象的場景中。
在本文中,我們將爲你提供一個模板。
在特定問題中執行 BFS 之前確定結點和邊緣非常重要。通常,結點將是實際結點或是狀態,而邊緣將是實際邊緣或可能的轉換。
在這裏,我們爲你提供僞代碼作爲模板:
- 如代碼所示,在每一輪中,隊列中的結點是
等待處理的結點
。 - 在每個更外一層的
while
循環之後,我們距離根結點更遠一步
。變量step
指示從根結點到我們正在訪問的當前結點的距離。
/**
* Return the length of the shortest path between root and target node.
*/
int BFS(Node root, Node target) {
Queue<Node> queue; // store all nodes which are waiting to be processed
int step = 0; // number of steps neeeded from root to current node
// initialize
add root to queue;
// BFS
while (queue is not empty) {
step = step + 1;
// iterate the nodes which are already in the queue
int size = queue.size();
for (int i = 0; i < size; ++i) {
Node cur = the first node in queue;
return step if cur is target;
for (Node next : the neighbors of cur) {
add next to queue;
}
remove the first node from queue;
}
}
return -1; // there is no path from root to target
}
有時,確保我們永遠不會訪問一個結點兩次
很重要。否則,我們可能陷入無限循環。如果是這樣,我們可以在上面的代碼中添加一個哈希集來解決這個問題。
這是修改後的僞代碼:
/**
* Return the length of the shortest path between root and target node.
*/
int BFS(Node root, Node target) {
Queue<Node> queue; // store all nodes which are waiting to be processed
Set<Node> used; // store all the used nodes
int step = 0; // number of steps neeeded from root to current node
// initialize
add root to queue;
add root to used;
// BFS
while (queue is not empty) {
step = step + 1;
// iterate the nodes which are already in the queue
int size = queue.size();
for (int i = 0; i < size; ++i) {
Node cur = the first node in queue;
return step if cur is target;
for (Node next : the neighbors of cur) {
if (next is not in used) {
add next to queue;
add next to used;
}
}
remove the first node from queue;
}
}
return -1; // there is no path from root to target
}