遞歸算法(recursion algorithm)在計算機科學中是指一種通過重復將問題分解為同類的子問題而解決問題的方法。
通俗的說就是能把大問題等價於一個小問題的循環重復,從而通過解決一個小問題來達到解決大問題的目的。
這里的循環重復,和普通的loop 語句不太一樣,在代碼中體現為方法的自調用。
眾所周知,循環的過程必須有一個控制條件來斷開循環,否則就會無限循環下去。
所以,能夠使用且應該使用遞歸算法的應用場景,個人歸納為三點:
1. 大問題能拆分等價於小問題的循環重復(必須)
2. 有控制條件(稱為出口)來斷開自我調用,或者繼續自我調用,控制條件並不一定是簡單的判斷語句,可以有多種情況或者多個條件(必須)
3. 一次自調用的結果,應該是下一次調用的初始值
讓我們來看幾個例子,並且對比使用遞歸算法以及不使用遞歸的差異,了解一下使用遞歸的心路歷程。
1. 階乘
//n*(n-1)*(n-1-1)*(n-1-1-1)... //解析n *(n-1) *(n-1-1) //階乘的過程歸納為一個乘法運算的重復 *(n-1) public static double FactorialWithRecursion(int n) { var result = 1; int temp; if (n > 1) { result = n * (n - 1); { temp = n - 1; if (temp > 1)//可以看到往下都是一個重復的過程,可以看作是一個遞歸方法調用的展開 { result = result * (temp - 1); temp = temp - 1; if (temp > 1) { result = result * (temp - 1); temp = temp - 1; if (temp > 1) { result = result * (temp - 1);//可以把重復的代碼抽離出來寫出一個方法 ... } } } } return result; } else { return result; } }
經過抽離重復的代碼,可以改進為如下。
public static double FactorialWithRecursion(int n) { if (n > 1)//明確終止條件 { Console.Write(n+"*"); return n * FactorialWithRecursion(n - 1); } else //當不滿足終止條件時的處理,即斷開自我循環調用的點 { Console.WriteLine(1); return 1; } }
2. 最經典的遞歸問題,斐波那契數列。
闡述: 斐波那契數列 1、1、2、3、5、8、13、21、34
斐波納契數列以如下被以遞歸的方法定義:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=2,n∈N*)
簡單的說,當n>=2時,當前數是前兩個數值相加的結果。
/// <summary> /// /// </summary> /// <param name="targetNumberIndex">指定斐波那契數列第幾位</param> /// <param name="previous">前一個數的值</param> /// <param name="currentResult">當前累加結果</param> /// <param name="currentNumberIndex">當前累加到第幾位</param> /// <returns></returns> public static double FibonacciWithRecursion(int targetNumberIndex, int previous = 1, int currentResult = 1, int currentNumberIndex = 2) {
if (targetNumberIndex > 2 && currentNumberIndex <= targetNumberIndex) { currentNumberIndex += 1; Console.WriteLine(currentResult); var temp = currentResult; currentResult = currentResult + previous; return FibonacciWithRecursion(targetNumberIndex, temp, currentResult, currentNumberIndex); } else { return 1; } }
這邊采用的是一個正向思維,即F(n) = F(n-1)+F(n-2). 上圖中並不是一個優秀的實現,但是便於理解。
重復調用的過程歸納為,result = previous + result;
在這個例子當中,就體現了我在上文提到的第三點,一次函數調用的結果,是下一次函數調用的初始值。
3. 應用題
題目:一個人趕着鴨子去每個村庄賣,每經過一個村子賣去所趕鴨子的一半又一只。這樣他經過了七個村子后還剩兩只鴨子,問他出發時共趕多少只鴨子?
1. 首先我們發現題目中有句話“每經過一個村子賣去所趕鴨子的一半又一只”,可以看出這句話表明了這是一個重復的過程。
2. 在分析上面那句話,“賣去所趕鴨子的一半又一只”,這表明了重復的計算方式,但是這里有兩種思維方式:
1) 計算賣出去的鴨子數量,y=x/2+1;
2)計算剩下的鴨子數量, y=x/2-1;
3. 再看“經過了七個村子后還剩兩只鴨子,問他出發時共趕多少只鴨子“,可以得出這是一個根據結果推出初始條件的問題。
假設初始有X只鴨子,等式為 [(x/2-1)/2 -1]/2 -1...= 2; 重復過程為result =(result/2 -1), 出口即為重復天數,直到天數為零
如果從結果推出初始條件,等式可以寫為,[(2+1)*2+1]*2 +1... = x; 重復過程為result = (result*2 + 1),出口為重復天數,直到天數等於給予的天數7
讀者可以自己嘗試着去實現這兩種不同的方式。
最后附上一張自制體現遞歸過程的圖
各種算法的思想,永遠是程序猿的必備利器。