棧——後入先出的數據結構(LIFO)

在 LIFO 數據結構中,將首先處理添加到隊列中的最新元素

與隊列不同,棧是一個 LIFO 數據結構。通常,插入操作在棧中被稱作入棧 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;
}

從現在開始,我們可以使用內置的棧庫來更方便地解決問題。 讓我們從一個有趣的問題(最小棧)開始,幫助你複習有用的操作。 然後我們將看一些經典的棧問題。 當你想首先處理最後一個元素時,棧將是最合適的數據結構。

我們希望通過 DFS 找出從根結點 A 到目標結點 G 的路徑。
在這裏插入圖片描述

1. 結點的處理順序是什麼?

在上面的例子中,我們從根結點 A 開始。首先,我們選擇結點 B 的路徑,並進行回溯,直到我們到達結點 E,我們無法更進一步深入。然後我們回溯到 A 並選擇第二條路徑到結點 C 。從 C 開始,我們嘗試第一條路徑到 E 但是 E 已被訪問過。所以我們回到 C 並嘗試從另一條路徑到 F。最後,我們找到了 G

總的來說,在我們到達最深的結點之後,我們會回溯並嘗試另一條路徑。

因此,你在 DFS 中找到的第一條路徑並不總是最短的路徑。例如,在上面的例子中,我們成功找出了路徑 A-> C-> F-> G 並停止了 DFS。但這不是從 AG 的最短路徑。

2. 棧的入棧和退棧順序是什麼?

如上面的動畫所示,我們首先將根結點推入到棧中;然後我們嘗試第一個鄰居 B 並將結點 B 推入到棧中;等等等等。當我們到達最深的結點 E 時,我們需要回溯。當我們回溯時,我們將從棧中彈出最深的結點,這實際上是推入到棧中的最後一個結點

結點的處理順序是完全相反的順序,就像它們被添加到棧中一樣,它是後進先出(LIFO)。這就是我們在 DFS 中使用棧的原因。

DFS - 模板 I

正如我們在本章的描述中提到的,在大多數情況下,我們在能使用 BFS 時也可以使用 DFS。但是有一個重要的區別:遍歷順序

與 BFS 不同,更早訪問的結點可能不是更靠近根結點的結點。因此,你在 DFS 中找到的第一條路徑可能不是最短路徑

在本文中,我們將爲你提供一個 DFS 的遞歸模板,並向你展示棧是如何幫助這個過程的。在這篇文章之後,我們會提供一些練習給大家練習。

模板 - 遞歸

有兩種實現 DFS 的方法。第一種方法是進行遞歸,這一點你可能已經很熟悉了。這裏提供了一個模板作爲參考:

/*
 * 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)。

在每個堆棧元素中,都有一個整數 cur,一個整數 target,一個對訪問過的數組的引用和一個對數組邊界的引用,這些正是我們在 DFS 函數中的參數。我們只在上面的棧中顯示 cur

每個元素都需要固定的空間。棧的大小正好是 DFS 的深度。因此,在最壞的情況下,維護系統棧需要 O(h),其中 h 是 DFS 的最大深度。在計算空間複雜度時,永遠不要忘記考慮系統棧。

在上面的模板中,我們在找到第一條路徑時停止。

如果你想找到最短路徑呢?

提示:再添加一個參數來指示你已經找到的最短路徑。

DFS - 模板

遞歸解決方案的優點是它更容易實現。 但是,存在一個很大的缺點:如果遞歸的深度太高,你將遭受堆棧溢出。 在這種情況下,您可能會希望使用 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;
}

資料來自
https://leetcode-cn.com/explore/learn/card/

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