當某個數據集合只涉及在一端插入和刪除數據,並且滿足後進先出、先進後出的特性,我們就應該首選“棧”這種數據結構。
如何實現一個“棧”?
注意:從數組中刪除的操作 ,其實並不需要真正的把某個值刪除,只要把我們訪問下標給減一就可以了。
class Stack {
public:
int n;
int count;
int *data;
Stack(int num) :n(num),count(0) {
data = new int[n];
}
~Stack(){
delete data;
}
bool push(int k) {
if (n == count) {
return false;
}
data[count++] = k;
return true;
}
int pop() {
if (count == 0) return 0;
int t = data[count-1];
count--;
return t;
}
};
int main() {
return 0;
}
支持動態擴容的順序棧
當數組空間不夠時,我們就重新申請一塊更大的內存,將原來數組中數據統統拷貝過去。這樣就實現了一個支持動態擴容的數組。如果要實現一個支持動態擴容的棧,我們只需要底層依賴一個支持動態擴容的數組就可以了。當棧滿了之後,我們就申請一個更大的數組,將原來的數據搬移到新數組中。
支持動態擴容的棧的push、pop時間複雜度
根據均攤法,前k次入棧直接push,第k+1次入棧,需要把前面k個數據全部進行搬移,所以均攤下到前面k次,每次入棧實際上 是一次直接入棧和一次數據搬移,時間複雜度:O(1)。
疑問:數據搬移的時間複雜度是O(1)嗎?
- 棧在函數調用中的應用
每調用一個函數,OS都會分配一塊棧空間,用來存儲函數調用時的臨時變量。
疑問:糾結一個問題,當調用一個函數時候,在棧上開闢一塊空間,如果再調用一個函數,那麼再開闢一個棧空間,兩個空間之間怎麼區別開呢?並且在函數中,如果連續兩個局部變量存儲,它們之間的位置關係是連續的嗎?看了下面博客懂了一些,不過還是買到這本書再把這點弄清楚吧。
https://www.jianshu.com/p/b666213cdd8a
- 棧在表達式求值中的應用
編譯器就是通過兩個棧來實現的。其中一個保存操作數的棧,另一個是保存運算符的棧。我們從左向右遍歷表達式,當遇到數字,我們就直接壓入操作數棧;當遇到運算符,就與運算符棧的棧頂元素進行比較。如果比運算符棧頂元素的優先級高,就將當前運算符壓入棧;如果比運算符棧頂元素的優先級低或者相同,從運算符棧中取棧頂運算符,從操作數棧的棧頂取 2 個操作數,然後進行計算,再把計算完的結果壓入操作數棧,繼續比較。
- 棧在括號匹配中的應用
如何實現瀏覽器的前進、後退功能?
我們使用兩個棧,X 和 Y,我們把首次瀏覽的頁面依次壓入棧 X,當點擊後退按鈕時,再依次從棧 X 中出棧,並將出棧的數據依次放入棧 Y。當我們點擊前進按鈕時,我們依次從棧 Y 中取出數據,放入棧 X 中。當棧 X 中沒有數據時,那就說明沒有頁面可以繼續後退瀏覽了。當棧 Y 中沒有數據,那就說明沒有頁面可以點擊前進按鈕瀏覽了。
比如你順序查看了 a,b,c 三個頁面,我們就依次把 a,b,c 壓入棧,這個時候,兩個棧的數據就是圖(1);
當你通過瀏覽器的後退按鈕,從頁面 c 後退到頁面 a 之後,我們就依次把 c 和 b 從棧 X 中彈出,並且依次放入到棧 Y。這個時候,兩個棧的數據就是圖(2);
這個時候,你通過頁面 b 又跳轉到新的頁面 d 了,頁面 c 就無法再通過前進、後退按鈕重複查看了,所以需要清空棧 Y。此時兩個棧的數據就是圖(3)。
課後思考:
爲什麼函數調用要用“棧”來保存臨時變量呢?
其實,我們不一定非要用棧來保存臨時變量,只不過如果這個函數調用符合後進先出的特性,用棧這種數據結構來實現,是最順理成章的選擇。
從調用函數進入被調用函數,對於數據來說,變化的是什麼呢?是作用域。所以根本上,只要能保證每進入一個新的函數,都是一個新的作用域就可以。而要實現這個,用棧就非常方便。在進入被調用函數的時候,分配一段棧空間給這個函數的變量,在函數結束的時候,將棧頂復位,正好回到調用函數的作用域內。