遞迴問題的簡單理解與解法
一、什麼是遞迴
在初學程式設計時,遞迴是一個比較難理解、難接受的問題。遞迴的執行方式和人的思考問題的方式不太一樣,相對於普通的選擇結構、迴圈結構,遞迴似乎給人一種把一個未知數變成了更多的未知數的感覺。但實際上,一旦理解並接受了遞迴解決問題的思路,普通的遞迴問題就會變得很容易。
為了文章的完整性,首先說一下什麼是遞迴。
遞迴(recursion)在電腦科學中是指一種通過重複將問題分解為同類的子問題而解決問題的方法。絕大多數程式語言支援函式的自呼叫,在這些語言中函式可以通過呼叫自身來進行遞迴。計算理論可以證明遞迴的作用可以完全取代迴圈,因此有很多在函式程式語言(如Scheme)中用遞迴來取代迴圈的例子。——wikipedia
一個方法(函式)自己呼叫自己就構成了遞迴:
public void recursion(){ recursion(); //自己呼叫自己,就叫遞迴 }
以上是一個簡單的JAVA遞迴,但是由於沒有設定中止條件,這個方法會無限呼叫自身,導致棧溢位。
二、遞迴的用途
遞迴主要用來解決以下三類問題:
1、資料的定義是按遞迴定義的。如Fibonacci函式。
2、問題解法按遞迴演算法實現。如Hanoi問題。
3、資料的結構形式是按遞迴定義的。如二叉樹、廣義表等。——wikipedia
斐波那契數列是理解遞迴問題的經典例子,它的每一項都等於前兩項之和,特別的,第1項為1,為方便定義,加上一個第“0”項是0。用公式表示就是:
F0 = 0
F1 = 1
Fn = F(n-1) + F(n-2) (n≥2)
當要求第n項的時候,有兩種思路:
第一種,從第2項開始求和,把第2、3、4、……、(n-2)、(n-1)項依次求出來,然後把第n-1 和 n-2項加起來得到第n項。
這種思路的特點是:從已知項出發,逐步向未知的方向推導,最終得出所求。這是我們解決問題的正常思路。
第二種,直接從第n項開始。我們所求的就是第n項,而第n項又有公式表示,我們把它一點一點展開,最終必然會展開到第1項和第0項。這就是遞迴的思路,根據這個思路,要求第n項,只需要求第n-1 和 n-2項,然後第n-1 和 n-2項又可以分解為第n-2 和 n-3項,第n-3和 n-4項,最終展開到第1項和第0項。
這種思路的特點是:從未知項出發,逐步向已知項展開。這就是遞迴的思路。
public class Fibo { public static void main(String[] args) { System.out.println(fibo1(0)); pintFibo1(20); pintFiboN(21); } // 思路一:從第0項、第1項開始加,一直加到第n項 public static int fibo1(int n) { // f0 f1 f2分別表示:第3k+0;3k+1,3k+2項,k為自然數 // n被3除,餘0就是f0,餘1就是f1,餘2就是f2 int f0 = 0; int f1 = 1; int f2 = f0 + f1; int f = 0;// 表示第n項數列的值。 if (n <= 1) { return n; } for (int i = 2; i <= n;) { f2 = f0 + f1; if (i++ == n) { f = f2; break; } f0 = f1 + f2; if (i++ == n) { f = f0; break; } f1 = f0 + f2; if (i++ == n) { f = f1; break; } } return f; } // 打印出利用第2項開始求和的方法求出的斐波那契數列 public static void pintFibo1(int n) { System.out.println("從第2項開始求前兩項的和,直到第n項:"); for (int i = 1; i <= n; i++) { System.out.print(fibo1(i) + " "); } System.out.println(); } // 思路二:從第N項開始展開。利用遞迴的方法求第n項斐波那契數。 public static int fiboN(int n) { if (n >= 2) { return fiboN(n - 1) + fiboN(n - 2);// 把第n項,用第(n - 1)、(n - 2)項表示 } else if (n == 0 || n == 1) { return n; // 一直展開到第n=1,n=0 } else { return n; } } // 打印出利用遞迴的方法求出的斐波那契數列 public static void pintFiboN(int n) { System.out.println("用遞迴求第n項: "); for (int i = 1; i <= n; i++) { System.out.print(fiboN(i) + " "); } System.out.println(); } }
顯然,使用遞迴的方式求斐波那契數列,比一步一步求和簡單得多。
三、常見遞迴問題的簡單理解
1.數列題
已知數列的某種規律,求第n項的值
這種題目一般是給出數列的通項公式,以及某具體項的具體值。
例題:
已知一個數列:
f(20) = 6765,
f(21) = 10946,
f(n) = f(n-1)+f(n-2),其中n是自然數。
求f(0),f(1)的值。
分析:在斐波那契數列中,我們已知f(0),f(1),求f(n),即“小項(0項,1項)”已知,“大項(n項)”未知,是從大項向小項遞迴。本題中,大項(20,21)已知,求小項(0,1),我們需要從小項向大項遞迴。但實際上它們的本質都是一樣的,都是從未知向已知遞迴。
解題步驟:
①求出通向公式,return這個公式
本題中,大項(20,21)已知,求小項(0,1),而通向公式是f(n) = f(n-1)+f(n-2),只能已知小項求大項,因此需要變換一下:
移項:f(n-2) = f(n)-f(n-1)
引數同時+2:f(n) = f(n+2)-f(n+1)
這樣我們就可以從大項向小項遞迴了。
程式碼如下
if (n < 20) { return f(n + 2) - f(n + 1); // 抄轉換後的公式 }
②return已知項
if (n == 20) { // 抄已知條件 return 6765; } if (n == 21) { // 抄已知條件 return 10946; }
到這裡就基本寫完了,完整程式碼如下:
public static int f(int n) { if (n < 20) return f(n + 2) - f(n + 1); // 抄轉換後的公式 else if (n == 20) // 抄已知條件 return 6765; else if (n == 21) // 抄已知條件 return 10946; else return 0; // 這個無實際意義,為了保證有返回值,實際上不會執行到 }
求得f(0)=0,f(1)=1。實際上由題目的通項公式f(n) = f(n-1)+f(n-2)可知,本質上就是斐波那契數列。
總結數列遞迴題的解法:
Ⅰ.根據通向公式,表示出f(n) 的公式。切記由未知向已知遞迴,確保最終能展開到已知項。
Ⅱ.寫出分支語句,根據n的取值,返回 f(n) 的公式,已知項。直接抄即可。
實際上用遞迴方法解決數列問題,主要就是“抄”:
public static int fibo(int n) { if (n >= 2) return fibo(n - 1) + fibo(n - 2); if (n == 0 || n == 1) return n; } //求斐波那契數列的程式碼,都是抄的題目已知條件
2.漢諾塔
漢諾塔問題的關鍵是:第n個盤子的移動,可以在前n-1個盤子移動的基礎上進行,即把前n-1個盤子看作一個整體。
然後前n-1個盤子,再看作前n-2個盤子的整體和第n-1個盤子
最終遞迴到兩個盤子和一個盤子的情況。
用遞迴解決問題的關鍵,就是找出第n步和前n-1步的關係。
四、遞迴正確性的驗證與數學歸納法
待補充