原作者:書呆子Rico 《遞歸的內涵與經典應用》 http://my.csdn.net/justloveyou_
摘要:
大師 L. Peter Deutsch 說過:To Iterate is Human, to Recurse, Divine.中文譯爲:人理解迭代,神理解遞歸。毋庸置疑地,遞歸確實是一個奇妙的思維方式。對一些簡單的遞歸問題,我們總是驚歎於遞歸描述問題的能力和編寫代碼的簡潔,但要想真正領悟遞歸的精髓、靈活地運用遞歸思想來解決問題卻並不是一件容易的事情。本文剖析了遞歸的思想內涵,分析了遞歸與循環的聯繫與區別,給出了遞歸的應用場景和一些典型應用,並利用遞歸和非遞歸的方式解決了包括階乘、斐波那契數列、漢諾塔、楊輝三角的存取、字符串迴文判斷、字符串全排列、二分查找、樹的深度求解在內的八個經典問題。
版權聲明:
本文原創作者:書呆子Rico
作者博客地址:http://blog.csdn.net/justloveyou_/
友情提示:
若讀者需要本博文相關完整代碼,請移步我的Github自行獲取,項目名爲 SwordtoOffer,鏈接地址爲:https://github.com/githubofrico/SwordtoOffer。
一. 引子
大師 L. Peter Deutsch 說過:To Iterate is Human, to Recurse, Divine.中文譯爲:人理解迭代,神理解遞歸。毋庸置疑地,遞歸確實是一個奇妙的思維方式。對一些簡單的遞歸問題,我們總是驚歎於遞歸描述問題的能力和編寫代碼的簡潔,但要想真正領悟遞歸的精髓、靈活地運用遞歸思想來解決問題卻並不是一件容易的事情。在正式介紹遞歸之前,我們首先引用知乎用戶李繼剛(https://www.zhihu.com/question/20507130/answer/15551917)對遞歸和循環的生動解釋:
遞歸:你打開面前這扇門,看到屋裏面還有一扇門。你走過去,發現手中的鑰匙還可以打開它,你推開門,發現裏面還有一扇門,你繼續打開它。若干次之後,你打開面前的門後,發現只有一間屋子,沒有門了。然後,你開始原路返回,每走回一間屋子,你數一次,走到入口的時候,你可以回答出你到底用這你把鑰匙打開了幾扇門。
循環:你打開面前這扇門,看到屋裏面還有一扇門。你走過去,發現手中的鑰匙還可以打開它,你推開門,發現裏面還有一扇門(若前面兩扇門都一樣,那麼這扇門和前兩扇門也一樣;如果第二扇門比第一扇門小,那麼這扇門也比第二扇門小,你繼續打開這扇門,一直這樣繼續下去直到打開所有的門。但是,入口處的人始終等不到你回去告訴他答案。
上面的比喻形象地闡述了遞歸與循環的內涵,那麼我們來思考以下幾個問題:
什麼是遞歸呢?
遞歸的精髓(思想)是什麼?
遞歸和循環的區別是什麼?
什麼時候該用遞歸?
使用遞歸需要注意哪些問題?
遞歸思想解決了哪些經典的問題?
這些問題正是筆者準備在本文中詳細闡述的問題。
二. 遞歸的內涵
1、定義 (什麼是遞歸?)
在數學與計算機科學中,遞歸(Recursion)是指在函數的定義中使用函數自身的方法。實際上,遞歸,顧名思義,其包含了兩個意思:遞 和 歸,這正是遞歸思想的精華所在。
2、遞歸思想的內涵(遞歸的精髓是什麼?)
正如上面所描述的場景,遞歸就是有去(遞去)有回(歸來),如下圖所示。“有去”是指:遞歸問題必須可以分解爲若干個規模較小,與原問題形式相同的子問題,這些子問題可以用相同的解題思路來解決,就像上面例子中的鑰匙可以打開後面所有門上的鎖一樣;“有回”是指 : 這些問題的演化過程是一個從大到小,由近及遠的過程,並且會有一個明確的終點(臨界點),一旦到達了這個臨界點,就不用再往更小、更遠的地方走下去。最後,從這個臨界點開始,原路返回到原點,原問題解決。
更直接地說,遞歸的基本思想就是把規模大的問題轉化爲規模小的相似的子問題來解決。特別地,在函數實現時,因爲解決大問題的方法和解決小問題的方法往往是同一個方法,所以就產生了函數調用它自身的情況,這也正是遞歸的定義所在。格外重要的是,這個解決問題的函數必須有明確的結束條件,否則就會導致無限遞歸的情況。
3、用歸納法來理解遞歸
數學都不差的我們,第一反應就是遞歸在數學上的模型是什麼,畢竟我們對於問題進行數學建模比起代碼建模拿手多了。觀察遞歸,我們會發現,遞歸的數學模型其實就是 數學歸納法,這個在高中的數列裏面是最常用的了,下面回憶一下數學歸納法。
數學歸納法適用於將解決的原問題轉化爲解決它的子問題,而它的子問題又變成子問題的子問題,而且我們發現這些問題其實都是一個模型,也就是說存在相同的邏輯歸納處理項。當然有一個是例外的,也就是歸納結束的那一個處理方法不適用於我們的歸納處理項,當然也不能適用,否則我們就無窮歸納了。總的來說,歸納法主要包含以下三個關鍵要素:
步進表達式:問題蛻變成子問題的表達式
結束條件:什麼時候可以不再使用步進表達式
直接求解表達式:在結束條件下能夠直接計算返回值的表達式
事實上,這也正是某些數學中的數列問題在利用編程的方式去解決時可以使用遞歸的原因,比如著名的斐波那契數列問題。
4、遞歸的三要素
在我們瞭解了遞歸的基本思想及其數學模型之後,我們如何才能寫出一個漂亮的遞歸程序呢?筆者認爲主要是把握好如下三個方面:
1、明確遞歸終止條件;
2、給出遞歸終止時的處理辦法;
3、提取重複的邏輯,縮小問題規模。
- 1
- 2
- 3
- 4
- 5
1). 明確遞歸終止條件
我們知道,遞歸就是有去有回,既然這樣,那麼必然應該有一個明確的臨界點,程序一旦到達了這個臨界點,就不用繼續往下遞去而是開始實實在在的歸來。換句話說,該臨界點就是一種簡單情境,可以防止無限遞歸。
2). 給出遞歸終止時的處理辦法
我們剛剛說到,在遞歸的臨界點存在一種簡單情境,在這種簡單情境下,我們應該直接給出問題的解決方案。一般地,在這種情境下,問題的解決方案是直觀的、容易的。
3). 提取重複的邏輯,縮小問題規模*
我們在闡述遞歸思想內涵時談到,遞歸問題必須可以分解爲若干個規模較小、與原問題形式相同的子問題,這些子問題可以用相同的解題思路來解決。從程序實現的角度而言,我們需要抽象出一個乾淨利落的重複的邏輯,以便使用相同的方式解決子問題。
5、遞歸算法的編程模型
在我們明確遞歸算法設計三要素後,接下來就需要着手開始編寫具體的算法了。在編寫算法時,不失一般性,我們給出兩種典型的遞歸算法設計模型,如下所示。
模型一: 在遞去的過程中解決問題
function recursion(大規模){
if (end_condition){ // 明確的遞歸終止條件
end; // 簡單情景
}else{ // 在將問題轉換爲子問題的每一步,解決該步中剩餘部分的問題
solve; // 遞去
recursion(小規模); // 遞到最深處後,不斷地歸來
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
模型二: 在歸來的過程中解決問題
function recursion(大規模){
if (end_condition){ // 明確的遞歸終止條件
end; // 簡單情景
}else{ // 先將問題全部描述展開,再由盡頭“返回”依次解決每步中剩餘部分的問題
recursion(小規模); // 遞去
solve; // 歸來
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
6、遞歸的應用場景
在我們實際學習工作中,遞歸算法一般用於解決三類問題:
(1). 問題的定義是按遞歸定義的(Fibonacci函數,階乘,…);
(2). 問題的解法是遞歸的(有些問題只能使用遞歸方法來解決,例如,漢諾塔問題,…);
(3). 數據結構是遞歸的(鏈表、樹等的操作,包括樹的遍歷,樹的深度,…)。
在下文我們將給出遞歸算法的一些經典應用案例,這些案例基本都屬於第三種類型問題的範疇。
三. 遞歸與循環
遞歸與循環是兩種不同的解決問題的典型思路。遞歸通常很直白地描述了一個問題的求解過程,因此也是最容易被想到解決方式。循環其實和遞歸具有相同的特性,即做重複任務,但有時使用循環的算法並不會那麼清晰地描述解決問題步驟。單從算法設計上看,遞歸和循環並無優劣之別。然而,在實際開發中,因爲函數調用的開銷,遞歸常常會帶來性能問題,特別是在求解規模不確定的情況下;而循環因爲沒有函數調用開銷,所以效率會比遞歸高。遞歸求解方式和循環求解方式往往可以互換,也就是說,如果用到遞歸的地方可以很方便使用循環替換,而不影響程序的閱讀,那麼替換成循環往往是好的。問題的遞歸實現轉換成非遞歸實現一般需要兩步工作:
(1). 自己建立“堆棧(一些局部變量)”來保存這些內容以便代替系統棧,比如樹的三種非遞歸遍歷方式;
(2). 把對遞歸的調用轉變爲對循環處理。
特別地,在下文中我們將給出遞歸算法的一些經典應用案例,對於這些案例的實現,我們一般會給出遞歸和非遞歸兩種解決方案,以便讀者體會。
四. 經典遞歸問題實戰
- 第一類問題:問題的定義是按遞歸定義的
(1). 階乘
/**
* Title: 階乘的實現
* Description:
* 遞歸解法
* 非遞歸解法
* @author rico
*/
public class Factorial {
/**
* @description 階乘的遞歸實現
* @author rico
* @created 2017年5月10日 下午8:45:48
* @param n
* @return
*/
public static long f(int n){
if(n == 1) // 遞歸終止條件
return 1; // 簡單情景
return n*f(n-1); // 相同重複邏輯,縮小問題的規模
}
--------------------------------我是分割線-------------------------------------
/**
* @description 階乘的非遞歸實現
* @author rico
* @created 2017年5月10日 下午8:46:43
* @param n
* @return
*/
public static long f_loop(int n) {
long result = n;
while (n > 1) {
n--;
result = result * n;
}
return result;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
(2). 斐波納契數列
/**
* Title: 斐波納契數列
*
* Description: 斐波納契數列,又稱黃金分割數列,指的是這樣一個數列:1、1、2、3、5、8、13、21、……
* 在數學上,斐波納契數列以如下被以遞歸的方法定義:F0=0,F1=1,Fn=F(n-1)+F(n-2)(n>=2,n∈N*)。
*
* 兩種遞歸解法:經典解法和優化解法
* 兩種非遞歸解法:遞推法和數組法
*
* @author rico
*/
public class FibonacciSequence {
/**
* @description 經典遞歸法求解
*
* 斐波那契數列如下:
*
* 1,1,2,3,5,8,13,21,34,...
*
* *那麼,計算fib(5)時,需要計算1次fib(4),2次fib(3),3次fib(2),調用了2次fib(1)*,即:
*
* fib(5) = fib(4) + fib(3)
*
* fib(4) = fib(3) + fib(2) ;fib(3) = fib(2) + fib(1)
*
* fib(3) = fib(2) + fib(1)
*
* 這裏麪包含了許多重複計算,而實際上我們只需計算fib(4)、fib(3)、fib(2)和fib(1)各一次即可,
* 後面的optimizeFibonacci函數進行了優化,使時間複雜度降到了O(n).
*
* @author rico
* @created 2017年5月10日 下午12:00:42
* @param n
* @return
*/
public static int fibonacci(int n) {
if (n == 1 || n == 2) { // 遞歸終止條件
return 1; // 簡單情景
}
return fibonacci(n - 1) + fibonacci(n - 2); // 相同重複邏輯,縮小問題的規模
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
——————————–我是分割線————————————-
/**
* @description 對經典遞歸法的優化
*
* 斐波那契數列如下:
*
* 1,1,2,3,5,8,13,21,34,...
*
* 那麼,我們可以這樣看:fib(1,1,5) = fib(1,2,4) = fib(2,3,3) = 5
*
* 也就是說,以1,1開頭的斐波那契數列的第五項正是以1,2開頭的斐波那契數列的第四項,
* 而以1,2開頭的斐波那契數列的第四項也正是以2,3開頭的斐波那契數列的第三項,
* 更直接地,我們就可以一步到位:fib(2,3,3) = 2 + 3 = 5,計算結束。
*
* 注意,前兩個參數是數列的開頭兩項,第三個參數是我們想求的以前兩個參數開頭的數列的第幾項。
*
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
* 時間複雜度:O(n)
*
* @author rico
* @param first 數列的第一項
* @param second 數列的第二項
* @param n 目標項
* @return
*/
public static int optimizeFibonacci(int first, int second, int n) {
if (n > 0) {
if(n == 1){ // 遞歸終止條件
return first; // 簡單情景
}else if(n == 2){ // 遞歸終止條件
return second; // 簡單情景
}else if (n == 3) { // 遞歸終止條件
return first + second; // 簡單情景
}
return optimizeFibonacci(second, first + second, n - 1); // 相同重複邏輯,縮小問題規模
}
return -1;
}
--------------------------------我是分割線-------------------------------------
/**
* @description 非遞歸解法:有去無回
* @author rico
* @created 2017年5月10日 下午12:03:04
* @param n
* @return
*/
public static int fibonacci_loop(int n) {
if (n == 1 || n == 2) {
return 1;
}
int result = -1;
int first = 1; // 自己維護的"棧",以便狀態回溯
int second = 1; // 自己維護的"棧",以便狀態回溯
for (int i = 3; i <= n; i++) { // 循環
result = first + second;
first = second;
second = result;
}
return result;
}
--------------------------------我是分割線-------------------------------------
/**
* @description 使用數組存儲斐波那契數列
* @author rico
* @param n
* @return
*/
public static int fibonacci_array(int n) {
if (n > 0) {
int[] arr = new int[n]; // 使用臨時數組存儲斐波納契數列
arr[0] = arr[1] = 1;
for (int i = 2; i < n; i++) { // 爲臨時數組賦值
arr[i] = arr[i-1] + arr[i-2];
}
return arr[n - 1];
}
return -1;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
(3). 楊輝三角的取值
/**
* @description 遞歸獲取楊輝三角指定行、列(從0開始)的值
* 注意:與是否創建楊輝三角無關
- 1
- 2
- 3
* @author rico
* @x 指定行
* @y 指定列
*/
/**
* Title: 楊輝三角形又稱Pascal三角形,它的第i+1行是(a+b)i的展開式的係數。
* 它的一個重要性質是:三角形中的每個數字等於它兩肩上的數字相加。
*
* 例如,下面給出了楊輝三角形的前4行:
* 1
* 1 1
* 1 2 1
* 1 3 3 1
* @description 遞歸獲取楊輝三角指定行、列(從0開始)的值
* 注意:與是否創建楊輝三角無關
* @author rico
* @x 指定行
* @y 指定列
*/
public static int getValue(int x, int y) {
if(y <= x && y >= 0){
if(y == 0 || x == y){ // 遞歸終止條件
return 1;
}else{
// 遞歸調用,縮小問題的規模
return getValue(x-1, y-1) + getValue(x-1, y);
}
}
return -1;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
(4). 迴文字符串的判斷
/**
* Title: 迴文字符串的判斷
* Description: 迴文字符串就是正讀倒讀都一樣的字符串。如”98789”, “abccba”都是迴文字符串
*
* 兩種解法:
* 遞歸判斷;
* 循環判斷;
*
* @author rico
*/
public class PalindromeString {
/**
* @description 遞歸判斷一個字符串是否是迴文字符串
* @author rico
* @created 2017年5月10日 下午5:45:50
* @param s
* @return
*/
public static boolean isPalindromeString_recursive(String s){
int start = 0;
int end = s.length()-1;
if(end > start){ // 遞歸終止條件:兩個指針相向移動,當start超過end時,完成判斷
if(s.charAt(start) != s.charAt(end)){
return false;
}else{
// 遞歸調用,縮小問題的規模
return isPalindromeString_recursive(s.substring(start+1).substring(0, end-1));
}
}
return true;
}
--------------------------------我是分割線-------------------------------------
/**
* @description 循環判斷迴文字符串
* @author rico
* @param s
* @return
*/
public static boolean isPalindromeString_loop(String s){
char[] str = s.toCharArray();
int start = 0;
int end = str.length-1;
while(end > start){ // 循環終止條件:兩個指針相向移動,當start超過end時,完成判斷
if(str[end] != str[start]){
return false;
}else{
end --;
start ++;
}
}
return true;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
(5). 字符串全排列
遞歸解法
/**
* @description 從字符串數組中每次選取一個元素,作爲結果中的第一個元素;然後,對剩餘的元素全排列
* @author rico
* @param s
* 字符數組
* @param from
* 起始下標
* @param to
* 終止下標
*/
public static void getStringPermutations3(char[] s, int from, int to) {
if (s != null && to >= from && to < s.length && from >= 0) { // 邊界條件檢查
if (from == to) { // 遞歸終止條件
System.out.println(s); // 打印結果
} else {
for (int i = from; i <= to; i++) {
swap(s, i, from); // 交換前綴,作爲結果中的第一個元素,然後對剩餘的元素全排列
getStringPermutations3(s, from + 1, to); // 遞歸調用,縮小問題的規模
swap(s, from, i); // 換回前綴,復原字符數組
}
}
}
}
/**
* @description 對字符數組中的制定字符進行交換
* @author rico
* @param s
* @param from
* @param to
*/
public static void swap(char[] s, int from, int to) {
char temp = s[from];
s[from] = s[to];
s[to] = temp;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
非遞歸解法(字典序全排列)
/**
* Title: 字符串全排列非遞歸算法(字典序全排列)
* Description: 字典序全排列,其基本思想是:
* 先對需要求排列的字符串進行字典排序,即得到全排列中最小的排列.
* 然後,找到一個比它大的最小的全排列,一直重複這一步直到找到最大值,即字典排序的逆序列.
*
* 不需要關心字符串長度
*
* @author rico
*/
public class StringPermutationsLoop {
/**
* @description 字典序全排列
*
* 設一個字符串(字符數組)的全排列有n個,分別是A1,A2,A3,...,An
*
* 1. 找到最小的排列 Ai
* 2. 找到一個比Ai大的最小的後繼排列Ai+1
* 3. 重複上一步直到沒有這樣的後繼
*
* 重點就是如何找到一個排列的直接後繼:
* 對於字符串(字符數組)a0a1a2……an,
* 1. 從an到a0尋找第一次出現的升序排列的兩個字符(即ai < ai+1),那麼ai+1是一個極值,因爲ai+1之後的字符爲降序排列,記 top=i+1;
* 2. 從top處(包括top)開始查找比ai大的最小的值aj,記 minMax = j;
* 3. 交換minMax處和top-1處的字符;
* 4. 翻轉top之後的字符(包括top),即得到一個排列的直接後繼排列
*
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
* @author rico
* @param s
* 字符數組
* @param from
* 起始下標
* @param to
* 終止下標
*/
public static void getStringPermutations4(char[] s, int from, int to) {
Arrays.sort(s,from,to+1); // 對字符數組的所有元素進行升序排列,即得到最小排列
System.out.println(s);
char[] descendArr = getMaxPermutation(s, from, to); // 得到最大排列,即最小排列的逆序列
while (!Arrays.equals(s, descendArr)) { // 循環終止條件:迭代至最大排列
if (s != null && to >= from && to < s.length && from >= 0) { // 邊界條件檢查
int top = getExtremum(s, from, to); // 找到序列的極值
int minMax = getMinMax(s, top, to); // 從top處(包括top)查找比s[top-1]大的最小值所在的位置
swap(s, top - 1, minMax); // 交換minMax處和top-1處的字符
s = reverse(s, top, to); // 翻轉top之後的字符
System.out.println(s);
}
}
}
/**
* @description 對字符數組中的制定字符進行交換
* @author rico
* @param s
* @param from
* @param to
*/
public static void swap(char[] s, int from, int to) {
char temp = s[from];
s[from] = s[to];
s[to] = temp;
}
/**
* @description 獲取序列的極值
* @author rico
* @param s 序列
* @param from 起始下標
* @param to 終止下標
* @return
*/
public static int getExtremum(char[] s, int from, int to) {
int index = 0;
for (int i = to; i > from; i--) {
if (s[i] > s[i - 1]) {
index = i;
break;
}
}
return index;
}
/**
* @description 從top處查找比s[top-1]大的最小值所在的位置
* @author rico
* @created 2017年5月10日 上午9:21:13
* @param s
* @param top 極大值所在位置
* @param to
* @return
*/
public static int getMinMax(char[] s, int top, int to) {
int index = top;
char base = s[top-1];
char temp = s[top];
for (int i = top + 1; i <= to; i++) {
if (s[i] > base && s[i] < temp) {
temp = s[i];
index = i;
}
continue;
}
return index;
}
/**
* @description 翻轉top(包括top)後的序列
* @author rico
* @param s
* @param from
* @param to
* @return
*/
public static char[] reverse(char[] s, int top, int to) {
char temp;
while(top < to){
temp = s[top];
s[top] = s[to];
s[to] = temp;
top ++;
to --;
}
return s;
}
/**
* @description 根據最小排列得到最大排列
* @author rico
* @param s 最小排列
* @param from 起始下標
* @param to 終止下標
* @return
*/
public static char[] getMaxPermutation(char[] s, int from, int to) {
//將最小排列複製到一個新的數組中
char[] dsc = Arrays.copyOfRange(s, 0, s.length);
int first = from;
int end = to;
while(end > first){ // 循環終止條件
char temp = dsc[first];
dsc[first] = dsc[end];
dsc[end] = temp;
first ++;
end --;
}
return dsc;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
(6). 二分查找
/**
* @description 二分查找的遞歸實現
* @author rico
* @param array 目標數組
* @param low 左邊界
* @param high 右邊界
* @param target 目標值
* @return 目標值所在位置
*/
public static int binarySearch(int[] array, int low, int high, int target) {
//遞歸終止條件
if(low <= high){
int mid = (low + high) >> 1;
if(array[mid] == target){
return mid + 1; // 返回目標值的位置,從1開始
}else if(array[mid] > target){
// 由於array[mid]不是目標值,因此再次遞歸搜索時,可以將其排除
return binarySearch(array, low, mid-1, target);
}else{
// 由於array[mid]不是目標值,因此再次遞歸搜索時,可以將其排除
return binarySearch(array, mid+1, high, target);
}
}
return -1; //表示沒有搜索到
}
--------------------------------我是分割線-------------------------------------
/**
* @description 二分查找的非遞歸實現
* @author rico
* @param array 目標數組
* @param low 左邊界
* @param high 右邊界
* @param target 目標值
* @return 目標值所在位置
*/
public static int binarySearchNoRecursive(int[] array, int low, int high, int target) {
// 循環
while (low <= high) {
int mid = (low + high) >> 1;
if (array[mid] == target) {
return mid + 1; // 返回目標值的位置,從1開始
} else if (array[mid] > target) {
// 由於array[mid]不是目標值,因此再次遞歸搜索時,可以將其排除
high = mid -1;
} else {
// 由於array[mid]不是目標值,因此再次遞歸搜索時,可以將其排除
low = mid + 1;
}
}
return -1; //表示沒有搜索到
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 第二類問題:問題解法按遞歸算法實現
(1). 漢諾塔問題
/**
* Title: 漢諾塔問題
* Description:古代有一個梵塔,塔內有三個座A、B、C,A座上有64個盤子,盤子大小不等,大的在下,小的在上。
* 有一個和尚想把這64個盤子從A座移到C座,但每次只能允許移動一個盤子,並且在移動過程中,3個座上的盤子始終保持大盤在下,
* 小盤在上。在移動過程中可以利用B座。要求輸入層數,運算後輸出每步是如何移動的。
*
* @author rico
*/
public class HanoiTower {
/**
* @description 在程序中,我們把最上面的盤子稱爲第一個盤子,把最下面的盤子稱爲第N個盤子
* @author rico
* @param level:盤子的個數
* @param from 盤子的初始地址
* @param inter 轉移盤子時用於中轉
* @param to 盤子的目的地址
*/
public static void moveDish(int level, char from, char inter, char to) {
if (level == 1) { // 遞歸終止條件
System.out.println("從" + from + " 移動盤子" + level + " 號到" + to);
} else {
// 遞歸調用:將level-1個盤子從from移到inter(不是一次性移動,每次只能移動一個盤子,其中to用於週轉)
moveDish(level - 1, from, to, inter); // 遞歸調用,縮小問題的規模
// 將第level個盤子從A座移到C座
System.out.println("從" + from + " 移動盤子" + level + " 號到" + to);
// 遞歸調用:將level-1個盤子從inter移到to,from 用於週轉
moveDish(level - 1, inter, from, to); // 遞歸調用,縮小問題的規模
}
}
public static void main(String[] args) {
int nDisks = 30;
moveDish(nDisks, 'A', 'B', 'C');
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 第三類問題:數據的結構是按遞歸定義的
(1). 二叉樹深度
/**
* Title: 遞歸求解二叉樹的深度
* Description:
* @author rico
* @created 2017年5月8日 下午6:34:50
*/
public class BinaryTreeDepth {
/**
* @description 返回二叉數的深度
* @author rico
* @param t
* @return
*/
public static int getTreeDepth(Tree t) {
// 樹爲空
if (t == null) // 遞歸終止條件
return 0;
int left = getTreeDepth(t.left); // 遞歸求左子樹深度,縮小問題的規模
int right = getTreeDepth(t.left); // 遞歸求右子樹深度,縮小問題的規模
return left > right ? left + 1 : right + 1;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
(2). 二叉樹深度
/**
* @description 前序遍歷(遞歸)
* @author rico
* @created 2017年5月22日 下午3:06:11
* @param root
* @return
*/
- 1
- 2
- 3
- 4
- 5
- 6
- 7
public String preOrder(Node<E> root) {
StringBuilder sb = new StringBuilder(); // 存到遞歸調用棧
if (root == null) { // 遞歸終止條件
return ""; // ji
}else { // 遞歸終止條件
sb.append(root.data + " "); // 前序遍歷當前結點
sb.append(preOrder(root.left)); // 前序遍歷左子樹
sb.append(preOrder(root.right)); // 前序遍歷右子樹
return sb.toString();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
<link href="https://csdnimg.cn/release/phoenix/mdeditor/markdown_views-7b4cdcb592.css" rel="stylesheet">
</div>