在 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。但這不是從A
到G
的最短路徑。
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/