碎碎念
在大一學習C語言的時候,舉過一個用棧實現的括號匹配演算法,當時覺得很難,不過現在回顧起來,這個演算法也算是比較簡單的一個關於棧的應用了。而現在所常見的演算法問題也都是什麼中綴表示式轉字尾表示式,雙棧找最小值之類的。難度比之括號匹配稍有提升,不過倒也算是必須要掌握的演算法。
上述所說的表示式求值在程式設計語言中是一個最基本的問題,也是棧的實現的一個典型範例。
為什麼說是最基本? 我們知道,中綴表示式對於人來說是比較友好的,學過四則運算就可以對其求值,然而對於計算機來說,雖然也可以想辦法計算,但是卻算不上友好了;相反,字尾表示式雖然對人不友好,但是卻是計算機所喜歡的。
(話說,字尾表示式在編譯原理中的重要性也是能棲身前列的。)
棧
在C語言入門的時候,我們就會通過遞迴來求斐波那契數列,很簡單:
int fibonacci(int n) {
if (n==0 || n==1) return 1;
return fibonacci(n-1) + fibonacci(n-2);
}
複製程式碼
但是那時候還不懂原理,僅僅知道,遞迴就是函式呼叫其本身,但是接觸到資料結構的時候,再一次提出了遞迴的概念。
什麼是遞迴?遞迴就是函式呼叫其本身。
reverse(know) { // 1. go on
if (you know) return you know; // 2. look 4
else back to see the 1; // 3. go back to 1.
} // 4. you know what is recurision now.
複製程式碼
這時候,我們不僅知道遞迴真正的用法,同時也知道了一個事實,即遞迴程式的開銷通常很大,但與之相反的,其程式碼量又是非常少的。
通常情況下,我們會選擇將遞迴程式改寫成非遞迴程式,即所謂消除遞迴,但是當改寫後和改寫前的程式並不會有太大的效能提升,我們也沒有必要去改寫,比如:cout << fibonacci(5);
,為了這樣的呼叫去消除遞迴,有必要嗎?
可實際情況是,一個應用所要處理的資料並不算小,消除遞迴是不可避免的。
遞迴的精髓在於能否將原始的問題轉換為屬性相同,但問題規模較小的問題,學過演算法就知道,這同樣也是貪心策略和動態規劃的本質。
優化
對於遞迴程式的優化,我們通常會選擇棧做輔助,為什麼?我們知道,在作業系統中,有一種叫做**“函式呼叫堆疊”**的名詞,大概的解釋就是:當在某一函式A中呼叫另一函式B時,我們將A中的內容儲存後,壓入系統堆疊(你可以說這是在建立還原點,也可以說這個是現場保護,開心就好。),然後執行函式B的內容,當函式B執行結束後,將A從系統堆疊中彈出,繼續從斷點處執行,同時銷燬之前申請的棧空間。
同時,我們要知道,作業系統的主存是由空間上限的,不可能是無限的。系統堆疊的大小自然是受作業系統儲存空間大小的約束的,而且絕對小於系統儲存空間(不可能等於)。所以,當遞迴程式不斷申請棧空間到達系統棧所能分配的上限時,就有了所謂的“系統堆疊溢位”,即我們通常所說的“爆棧”。
- 斐波那契函式n=6時,遞迴呼叫樹
-
n=3時,棧的申請情況
-
n=6時,棧的申請情況
java中,異常java.lang.StackOverflowError就是一種堆疊溢位錯誤,不過,可以通過修改JVM引數來增大虛擬機器棧空間,如-vm args-Xss128k;但這也只是權宜之計,治標不治本吶。
但是呢,一個遞迴程式並不一定非要用棧輔助改寫成非遞迴程式(即消除遞迴),有時候,一個迴圈就夠了。
int main() {
int n, i=j=1, tmp=0;
cin >> n;
while (n--) {
tmp = i+j;
i = j;
j = tmp;
}
}
複製程式碼
暫時就說這麼多,至於後面的,那就後面再說吧,畢竟這也只是(一)嘛。