隊列——先入先出的數據結構(FIFO)

  • 隊列是典型的 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 之前確定結點和邊緣非常重要。通常,結點將是實際結點或是狀態,而邊緣將是實際邊緣或可能的轉換。

在這裏,我們爲你提供僞代碼作爲模板:

  1. 如代碼所示,在每一輪中,隊列中的結點是等待處理的結點
  2. 在每個更外一層的 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
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章