【LeetCode】算法與數據結構筆記(二) 隊列和棧

隊列

  隊列是先近先出的數據結構(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;
    }
}

  上述實現方法效率低下。 隨着起始指針的移動,浪費了越來越多的空間。 當我們有空間限制時,這將是難以接受的。

  • 循環隊列

  更有效的方法是循環隊列的方式。具體來說,我們可以使用固定大小的數組兩個指針來指示起始位置結束位置。 目的是重用我們之前提到的被浪費的存儲

設計循環隊列

class MyCircularQueue {

private:
    vector<int> data;
    int size;
    int head;
    int tail;
    int capacity;

public:
    /** Initialize your data structure here. Set the size of the queue to be k. */
    MyCircularQueue(int k) {
        data.resize(k);
        size=k;
        head=0;
        tail=0;
        capacity=0;
    }
    
    /** Insert an element into the circular queue. Return true if the operation is successful. */
    bool enQueue(int value) {
        if(capacity<size){
            data[tail]=value;
            tail = (tail+1)%size;
            ++capacity;
            return true;
        } else return false;
    }
    
    /** Delete an element from the circular queue. Return true if the operation is successful. */
    bool deQueue() {
        if(capacity>0){
            head=(head+1)%size;
            --capacity;
            return true;
        } else return false;
    }
    
    /** Get the front item from the queue. */
    int Front() {
        if(!isEmpty()){
            return data[head];
        } else return -1;
    }
    
    /** Get the last item from the queue. */
    int Rear() {
        if(!isEmpty()){
            return tail==0?data[size-1]:data[tail-1];
        } else return -1;
    }
    
    /** Checks whether the circular queue is empty or not. */
    bool isEmpty() {
        return capacity==0;
    }
    
    /** Checks whether the circular queue is full or not. */
    bool isFull() {
        return capacity==size;
    }
};

/**
 * 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();
 */

  這裏的程序要注意用Rear()函數取最後一個元素的時候,由於tail需要減1,因此當tail等於0的時候減1是會越界的,因此需要處理一下爲0的特殊情況。

  大多數的編程語言都有內置的隊列庫,我們無需重新造輪子。隊列最基本的兩個操作就是入隊(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;
}

隊列和廣度優先搜索

尋找根節點A和目標結點G之間的最短路徑

  若使用廣度優先搜索,第零輪中首先需要處理根節點,即A進入隊列;第一輪中處理根節點旁邊的結點BCD 近隊列,第二輪處理距離根結點兩步的結點後面依此類推。

  與樹的層序遍歷類似,越是接近根結點的結點將越早地遍歷

  如果在第 k輪中將結點 X添加到隊列中,則根結點與X之間的最短路徑的長度恰好是 k。也就是說,第一次找到目標結點時,你已經處於最短路徑中。

  結點的處理順序與它們添加到隊列的順序是完全相同的順序,即先進先出(FIFO)。這就是我們在 BFS 中使用隊列的原因。

  • 實現

  在程序執行BFS時,確定節點和邊緣是非常重要的。其大體的實現思路如下所示:

/**
 * 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
}

島嶼數量

  這道題目需要用到沉沒島嶼這個思想,當前位置座標爲1時,四周爲1的島嶼都需要置1沉沒。遞歸調用。下面提供廣度優先搜索代碼,用於理解題目意思和解題思路,之後再介紹深度優先搜索的時候再看與之不同點。

  • 廣度優先搜索
class Solution {
private:
    void bfs(vector<vector<char>>& grid, int row, int col){
        queue<pair<int, int>> neighbors;
        neighbors.push({row, col});
        while(!neighbors.empty()){
            auto rc = neighbors.front();
            neighbors.pop();
            int row = rc.first;
            int col=rc.second;
            if (row - 1 >= 0 && grid[row-1][col] == '1') {
                neighbors.push({row-1, col});
                grid[row-1][col] = '0';
            }
            if (row + 1 < grid.size() && grid[row+1][col] == '1') {
                neighbors.push({row+1, col});
                grid[row+1][col] = '0';
            }
            if (col - 1 >= 0 && grid[row][col-1] == '1') {
                neighbors.push({row, col-1});
                grid[row][col-1] = '0';
            }
            if (col + 1 < grid[0].size() && grid[row][col+1] == '1') {
                neighbors.push({row, col+1});
                grid[row][col+1] = '0';
            }
        }
    }
public:
    int numIslands(vector<vector<char>>& grid) {
        int ans=0;
        for(int row=0; row<grid.size();++row){
            for(int col=0;col<grid[0].size();++col){
                if(grid[row][col] == '1'){
                    ++ans;
                    bfs(grid, row, col);
                }
            }
        }
        return ans;
    }
};

  與隊列不同,棧是後入先出的數據結構。通常插入操作在棧中被稱作入棧push,與隊列類似,總是在堆棧的末尾添加一個新元素,但是,刪除操作,退棧pop,將始終刪除隊列中相對於它的最後一個元素

  棧的實現比隊列容易。動態數組足以實現堆棧結構。其原理實現如下所示:

#include <iostream>

class MyStack {
    private:
        vector<int> data;               // store elements
    public:
        /** Insert an element into the stack. */
        void push(int x) {
            data.push_back(x);
        }
        /** Checks whether the queue is empty or not. */
        bool isEmpty() {
            return data.empty();
        }
        /** Get the top item from the queue. */
        int top() {
            return data.back();
        }
        /** Delete an element from the queue. Return true if the operation is successful. */
        bool pop() {
            if (isEmpty()) {
                return false;
            }
            data.pop_back();
            return true;
        }
};

int main() {
    MyStack s;
    s.push(1);
    s.push(2);
    s.push(3);
    for (int i = 0; i < 4; ++i) {
        if (!s.isEmpty()) {
            cout << s.top() << endl;
        }
        cout << (s.pop() ? "true" : "false") << endl;
    }
}

  大多數流行的語言都提供了內置的棧庫,因此你不必重新發明輪子。除了初始化,我們還需要知道如何使用兩個最重要的操作:入棧退棧。除此之外,你應該能夠從棧中獲得頂部元素。下面是一些供你參考的代碼示例:

#include <iostream>

int main() {
    // 1. Initialize a stack.
    stack<int> s;
    // 2. Push new element.
    s.push(5);
    s.push(13);
    s.push(8);
    s.push(6);
    // 3. Check if stack is empty.
    if (s.empty()) {
        cout << "Stack is empty!" << endl;
        return 0;
    }
    // 4. Pop an element.
    s.pop();
    // 5. Get the top element.
    cout << "The top element is: " << s.top() << endl;
    // 6. Get the size of the stack.
    cout << "The size is: " << s.size() << endl;
}

棧和深度優先搜索

尋找根節點A和目標結點G之間的最短路徑

  同樣對於上圖所示這個問題,採用深度優先搜索算法的時候,我們從根結點 A 開始。首先,我們選擇結點 B 的路徑,並進行回溯,直到我們到達結點 E,我們無法更進一步深入。然後我們回溯到 A 並選擇第二條路徑到結點 C。從 C 開始,我們嘗試第一條路徑到 E 但是 E 已被訪問過。所以我們回到 C 並嘗試從另一條路徑到 F。最後,我們找到了 G

  在到達最深處的結點之後開始回溯,回溯時從棧中彈出最深的結點,所以也叫做深度優先搜索。但是DFS找到的路徑並不總是最短的路徑。

  以之前島嶼的例子舉例,大多數情況下能使用BFS時也可以使用DFS。與 BFS 不同,更早訪問的結點可能不是更靠近根結點的結點。因此,你在 DFS 中找到的第一條路徑可能不是最短路徑

  • 深度優先搜索
class Solution {
private:
    void dfs(vector<vector<char>>& grid, int row, int col){
            if(row<0||row>=grid.size()||col<0||col>=grid[0].size()
            ||grid[row][col]=='0'){
                return;
            } 
            else {
                grid[row][col] ='0';
                dfs(grid, row-1,col);
                dfs(grid, row+1,col);
                dfs(grid, row,col-1);
                dfs(grid, row,col+1);
            } 
        }
public:
    int numIslands(vector<vector<char>>& grid) {
        int ans=0;
        for(int row=0; row<grid.size();++row){
            for(int col=0;col<grid[0].size();++col){
                if(grid[row][col] == '1'){
                    ++ans;
                    dfs(grid, row, col);
                }
            }
        }
        return ans;
    }
};

  深度優先搜索模板可總結爲:

/*
 * Return true if there is a path from cur to target.
 */
boolean DFS(Node cur, Node target, Set<Node> visited) {
    return true if cur is target;
    for (next : each neighbor of cur) {
        if (next is not in visited) {
            add next to visted;
            return true if DFS(next, target, visited) == true;
        }
    }
    return false;
}

  當我們遞歸地實現 DFS 時,似乎不需要使用任何棧。但實際上,我們使用的是由系統提供的隱式棧,也稱爲調用棧(Call Stack)。

  棧的大小正好是DFS 的深度。因此,在最壞的情況下,維護系統棧需要 O(h)O(h),其中 hDFS 的最大深度。

  遞歸解決方案的優點是它更容易實現。 但是,存在一個很大的缺點:如果遞歸的深度太高,你將遭受堆棧溢出。 在這種情況下,您可能會希望使用 BFS,或使用顯式棧實現 DFS。

  使用顯式棧的模板:

/*
 * Return true if there is a path from cur to target.
 */
boolean DFS(int root, int target) {
    Set<Node> visited;
    Stack<Node> s;
    add root to s;
    while (s is not empty) {
        Node cur = the top element in s;
        return true if cur is target;
        for (Node next : the neighbors of cur) {
            if (next is not in visited) {
                add next to s;
                add next to visited;
            }
        }
        remove cur from s;
    }
    return false;
}

  該邏輯與遞歸解決方案完全相同。 但我們使用 while 循環和棧來模擬遞歸期間的系統調用棧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章