從漢諾塔問題來看“遞歸”本質


漢諾塔問題

大二上數據結構課,老師在講解“棧與遞歸的實現”時,引入了漢諾塔的問題,使用遞歸來解決n個盤在(x,y,z)軸上移動。

例如下面的動圖(圖片出自於漢諾塔算法詳解之C++):

三個盤的情況:

四個盤的情況:

如果是5個、6個、7個、...,該如何移動呢?

於是,老師給了一段經典的遞歸代碼:

void hanoi(int n,char x,char y,char z){
        if(n == 1)
                move(x,1,z);
        else{
                hanoi(n-1,x,z,y);
                move(x,n,z);
                hanoi(n-1,y,x,z);
        }
}

簡簡單單的幾句代碼里蘊含中深奧的秘密啊!

用圖像來講解一下這里面的原理吧,例如下圖:

(圖片來源於“碼農翻身”公眾號)

假設盤的序號從上往下增大,第一個盤序號為1,最后一個盤序號為n。每次只能移動一個盤,並且大盤不能在小盤的上面。那么運用遞歸的思想可知,若想將n號盤放到z軸上,那么必須先將(1,...,n-1)號盤移動到y軸上,此時z軸作為輔助軸。即

hanoi(n-1,x,z,y);

然后移動n號盤到z軸上,即

move(x,n,z);

最后將y軸上的(1,...,n-1)號盤移動到z軸上,此時x軸作為輔助軸。即

hanoi(n-1,y,x,z);

n的階乘問題

再說一個例子:計算n的階乘

f(n) = n!

其遞歸算法如下:

int factorial(int n){
     if(n == 1)
          return 1;
     else
          return n * factorial(n-1);  
}

這段程序加載到內存的分配圖如下:

(圖片來源於“碼農翻身”公眾號)

由於遞歸是函數自身調用自身,所以程序被編譯后代碼段中只有一份代碼。
遞歸調用是如何進行的呢?
注意看堆棧中的棧幀啊, 每個棧幀就代表了被調用中的一個函數, 這些函數棧幀以先進后出的方式排列起來,就形成了一個棧, 棧幀的結構如下圖所示:

(圖片來源於“碼農翻身”公眾號)

相信大家還記得《數據結構》(嚴蔚敏版)一書中提到的“工作記錄”就是指函數棧幀。棧頂指針被稱為“當前環境指針”。
忽略到其他內容, 只關注輸入參數和返回值的話,階乘函數factorial(4)的工作棧如下圖所示:

(圖片來源於“碼農翻身”公眾號)

其計算過程如下圖所示:

(圖片來源於“碼農翻身”公眾號)


注意, 每個遞歸函數必須得有個終止條件, 要不然就會發生無限遞歸了, 永遠都出不來了。

當然針對於此遞歸算法,對於n的值是有限制的。因為堆棧容量是有限的,如果n值太大程序會崩掉。

該如何解決呢?
從上面的代碼中可以知道“factorial(n) = n * factorial(n-1 ) ”  ,這個計算式是整個程序的核心。 圖中每個棧幀都需要記錄下當前的n的值, 還要記錄下一個函數棧幀的返回值, 然后才能運算出當前棧幀的結果。 也就是說使用多個棧幀是不可避免的。

可以使用下面的遞歸算法:

int factorial(int n,int result){
     if(n == 1){
          return result;
     }
     else{
          return factorial(n-1,n * result);
     }
}

注意函數的最后一個語句, 就不是 n * factorial(n-1) 了, 而是直接調用factorial(....) 這個函數本身,  這就帶來了巨大的好處。 

計算過程如下:

當執行到factorial(1, 24)的時候直接就可以返回結果了。
這就是妙處所在了,計算機發現這種情況,只用一個棧幀就可以搞定這些計算,無論n有多大。

(圖片來源於“碼農翻身”公眾號)

這就是所謂的“尾遞歸”了, 當遞歸調用是函數體中最后執行的語句並且它的返回值不屬於表達式一部分時, 這個遞歸就是尾遞歸。

現代的編譯器就會發現這個特點, 生成優化的代碼, 復用棧幀。 第一個算法中因為有個n * factorial(n-1) ,  雖然也是遞歸,但是遞歸的結果處於一個表達式中,還要做計算, 所以就沒法復用棧幀了,只能一層一層的調用下去。

另外,向大家推薦一個公眾號“碼農翻身”。上面有很多有關計算機方面的文章,淺顯易懂,十分受用。本文也在一定程度上,吸收了該公眾號上的精華。

“碼農翻身” 公共號 : 由工作15年的前IBM架構師創建,分享編程和職場的經驗教訓。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM