資料結構碎碎念(一)

大盜發表於2018-09-22

碎碎念


在大一學習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=6時,遞迴呼叫樹

  • n=3時,棧的申請情況

    n=3時,棧的申請情況

  • n=6時,棧的申請情況

    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;
	}
}
複製程式碼

暫時就說這麼多,至於後面的,那就後面再說吧,畢竟這也只是(一)嘛。

相關文章