如何使用遞歸來解決問題?

簡單的遞歸運算

有一個數組 a[5] = {10, 5, 4, 2, 1} ,利用遞歸運算來求和 sum

public int summation(int i) {
	if(i == 0)
		return a[0];
	return a[i] + summation(i-1);
}

遞歸運算可以拆分爲:
在這裏插入圖片描述
遞歸運算總是將一個問題的規模不斷減小,也可以說遞歸運算將一個問題拆分爲規模不同的相同問題。

遞歸有個顯著特點,會進行自己調用自己的函數,問題規模的減小主要通過函數的參數來實現。

遞歸和循環

遞歸和循環都是迭代運算,循環都可以轉換爲遞歸問題(比如數組求和),但是有的遞歸卻不能使用循環來解決。

遞歸運算相比於循環而言,雖然簡化了邏輯,但是程序的開銷增加,所以,簡單的迭代運算,我們如果能使用循環解決,儘量不要使用遞歸。(遞歸會調用函數,佔用棧空間)

涉及到問題規模不斷減小的迭代運算,我們優先選擇遞歸而不是循環。

遞歸運算的兩條法則:

  • 有基準情形(即遞歸結束的標誌,遞歸不能無休止地推進下去)
  • 不斷推進(遞歸總是能想着基準情形不斷推進)

Tips: 迭代運算最忌諱的就是沒有終止條件,基準情形可以說就是遞歸的終止條件

使用遞歸解決問題

這裏,我們舉幾個例子,斐波那契(fei bo na qi)數列:
斐波那契數列
明白人都看得出來,斐波那契數列給出了 基準情形,問題的規模也是不斷減小,必須得拿出遞歸來算啊。

基準情形:
  F(1) = 1;
  F(2) = 1;

遞推公式:
  F(n) = F(n -1) + F(n-2)

然後我們就能根據這些信息,寫出求斐波那契數列的方法:

public int getFibonacci(int n) {
	if(n == 1 || n == 2)
		return 1;
	else
		return getFibonacci(n-1) + getFibonacci(n-2);
}

下面是一個經典的遞歸例子,漢諾塔問題:
在這裏插入圖片描述
例如:
在這裏插入圖片描述
漢諾塔移動的思路,假設有 A B C 三個柱,目標是 B 柱:

  1. 將A中的 n - 1 個圓盤移動至 C 柱
  2. 將A中剩下的第 n 個圓盤移動至 B 柱
  3. 將 C 柱中的 n - 1 個圓盤移動至 B 柱

當走到步驟 3 的時候,又會從 步驟 1 開始執行,這樣我們就可以發現如何來實行遞歸

其中的基準情形爲:當柱中只有一個圓盤,直接移動至 目的柱即可。

圖1 a to b; a to c 圖2 b to c 圖3 a to b
圖4 c to a 圖5 c to b 圖6 a to b

弄清楚漢諾塔的遞歸過程,我們就可以嘗試寫出它的代碼了(通過輸出語句來打印圓盤的移動過程):

public class Test {
	public static int count;
	/*
	 * @param n 柱中圓盤個數
	 * @param A 代表移動原點的柱
	 * @param goal 代表將要到達的柱中
	 * @param B 輔助移動的柱子
	 */
	public static void setHanoi(int n, char A, char goal, char C) {
		count++;
		if(n == 1) {
			System.out.println(A + " to " + goal);
		}else{
			setHanoi(n - 1, A , C, goal);
			System.out.println(A + " to " + goal);
			setHanoi(n - 1, C, goal, A);
		}
	}
	public static void main(String[] args) {
		Test.setHanoi(3, 'a', 'b', 'c');
		System.out.println(count);
	}
}/*output:
a to b
a to c
b to c
a to b
c to a
c to b
a to b
7*/

如果,你對漢諾塔的遞歸問題有所疑問,可以下載一個 漢諾塔3D 遊戲輔助理解,同時也可以在遊戲中檢驗我們代碼的正確性。

在分析中我們可能遇到的問題:setHanoi()中的參數哪些是變化的哪些是不變的,這裏主要是通過參數順序的改變,來調換 出發點的柱子 和 目的地的柱子,代碼中的 goal 是變化的,而出發點也是變化的。

Tips: 使用遞推時,要搞清楚兩個東西,基準情形和遞推規律;在剛開始不要想着遞歸算法中的遞推會進行很多很多次,我們可以用規模較小的輸入來驗證自己的思路。

分析遞推深度

在學習遞歸思想的時候,我們最迷惑的可能就是遞推中的每一層是如何進行的,以及其他語句在遞歸中執行的先後順序:

  1. 在函數調用前的語句在遞歸前,從外層逐漸深入執行
  2. 在函數調用後的語句在遞推完成後,從深層向外執行

比如之前漢諾塔的遞歸運算,我們來看看它的深度變化:

public class Test {
	public static int count;
	/*
	 * @param n 柱中圓盤個數
	 * @param A 代表移動原點的柱
	 * @param goal 代表將要到達的柱中
	 * @param B 輔助移動的柱子
	 */
	public static void setHanoi(int n, char A, char goal, char C) {
		count++;
		if(n == 1) {
			System.out.println(getDepth(n)+A + " to " + goal);
		}else{
			setHanoi(n - 1, A , C, goal);
			System.out.println(getDepth(n)+A + " to " + goal);
			setHanoi(n - 1, C, goal, A);
		}
	}
	public static String getDepth(int n) {
		StringBuilder str = new StringBuilder();
		for(int i = 0; i < n; i++) {
			str.append("--");
		}
		return str.toString();
	}
	public static void main(String[] args) {
		Test.setHanoi(64, 'a', 'b', 'c');
		System.out.println(count);
	}
}

在這裏插入圖片描述
如果用 - 的個數代表圓盤的大小,你會發現遞歸的是多麼適用於漢諾塔,我們通過遞歸的深度解決了漢諾塔中圓盤大小規則(大圓盤不能放在小圓盤上)。

這個例子貌似並不能明顯看出深度的變化,我們在舉一個 求階乘的 例子:
在這裏插入圖片描述
編寫代碼:

package dg;

public class Test {
	public static int factorial(int n) {
		int f;
		if(n == 1)
			return 1;
		else {
			System.out.println(getDepth(n)+n);
			f = n * factorial(n-1);
			System.out.println(getDepth(n)+n);
			return f;
		}
	}
	public static String getDepth(int n) {
		StringBuilder str = new StringBuilder();
		for(int i = 0; i < n; i++) {
			str.append("--");
		}
		return str.toString();
	}
	public static void main(String[] args) {
		Test.factorial(5);
	}
}

在這裏插入圖片描述
通過這些,我們明顯觀察到,遞歸中函數調用前後語句的執行順序和深度。
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章