遞迴就這麼簡單
本來預算此章節是繼續寫快速排序的,然而編寫快速排序往往是遞迴來寫的,並且遞迴可能不是那麼好理解,於是就有了這篇文章。
在上面提到了遞迴這麼一個詞,遞迴在程式語言中簡單的理解是:方法自己呼叫自己
遞迴其實和迴圈是非常像的,迴圈都可以改寫成遞迴,遞迴未必能改寫成迴圈,這是一個充分不必要的條件。
- 那麼,有了迴圈,為什麼還要用遞迴呢??在某些情況下(費波納切數列,漢諾塔),使用遞迴會比迴圈簡單很多很多
- 話說多了也無益,讓我們來感受一下遞迴吧。
我們初學程式設計的時候肯定會做過類似的練習:
1+2+3+4+....+100(n)
- 給出一個數組,求該陣列內部的最大值
我們要記住的是,想要用遞迴必須知道兩個條件:
- 遞迴出口(終止遞迴的條件)
- 遞迴表示式(規律)
技巧:在遞迴中常常是將問題切割成兩個部分(1和整體的思想),這能夠讓我們快速找到遞迴表示式(規律)
一、求和
如果我們使用for
迴圈來進行求和1+2+3+4+....+100
,那是很簡單的:
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum = sum + i;
}
System.out.println ("公眾號:Java3y:" + sum);
前面我說了,for迴圈都可以使用遞迴來進行改寫,而使用遞迴必須要知道兩個條件:1、遞迴出口,2、遞迴表示式(規律)
首先,我們來找出它的規律:1+2+3+...+n
,這是一個求和的運算,那麼我們可以假設X=1+2+3+...+n
,可以將1+2+3+...+(n-1)
看成是一個整體。而這個整體做的事又和我們的初始目的(求和)相同。以我們的高中數學知識我們又可以將上面的式子看成X=sum(n-1)+n
好了,我們找到我們的遞迴表示式(規律),它就是sum(n-1)+n
,那遞迴出口呢,這個題目的遞迴出口就有很多了,我列舉一下:
- 如果
n=1
1
- 如果
n=2
時,那麼就返回3
(1+2) - 如果
n=3
時,那麼就返回6
(1+2+3)
當然了,我肯定是使用一個最簡單的遞迴出口了:if(n=1) return 1
遞迴表示式和遞迴出口我們都找到了,下面就程式碼演示:
遞迴出口為1:
public static void main(String[] args) {
System.out.println("公眾號:Java3y:" + sum(100));
}
/**
*
* @param n 要加到的數字,比如題目的100
* @return
*/
public static int sum(int n) {
if (n == 1) {
return 1;
} else {
return sum(n - 1) + n;
}
}
遞迴出口為4:
public static void main(String[] args) {
System.out.println("公眾號:Java3y:" + sum(100));
}
/**
*
* @param n 要加到的數字,比如題目的100
* @return
*/
public static int sum(int n) {
//如果遞迴出口為4,(1+2+3+4)
if (n == 4) {
return 10;
} else {
return sum(n - 1) + n;
}
}
結果都是一樣的。
二、陣列內部的最大值
如果使用的是迴圈,那麼我們通常這樣實現:
int[] arrays = {2, 3, 4, 5, 1, 5, 2, 9, 5, 6, 8, 3, 2};
//將陣列的第一個假設是最大值
int max = arrays[0];
for (int i = 1; i < arrays.length; i++) {
if (arrays[i] > max) {
max = arrays[i];
}
}
System.out.println("公眾號:Java3y:" + max);
那如果我們用遞迴的話,那怎麼用弄呢?首先還是先要找到遞迴表示式(規律)和遞迴出口
- 我們又可以運用1和整體的思想來找到規律
- 將陣列第一個數->
2
與陣列後面的數->{3, 4, 5, 1, 5, 2, 9, 5, 6, 8, 3, 2}
進行切割,將陣列後面的數看成是一個整體X={3, 4, 5, 1, 5, 2, 9, 5, 6, 8, 3, 2}
,那麼我們就可以看成是第一個數和一個整體進行比較if(2>X) return 2 else(2<X) return X
- 而我們要做的就是找出這個整體的最大值與
2
進行比較。找出整體的最大值又是和我們的初始目的(找出最大值)是一樣的 - 也就可以寫成
if( 2>findMax() )return 2 else return findMax()
- 將陣列第一個數->
- 遞迴出口,如果陣列只有1個元素時,那麼這個陣列最大值就是它了。
使用到陣列的時候,我們通常為陣列設定左邊界和右邊界,這樣比較好地進行切割
- L表示左邊界,往往表示的是陣列第一個元素,也就會賦值為0(角標為0是陣列的第一個元素)
- R表示右邊界,往往表示的是陣列的長度,也就會賦值為
arrays.length-1
(長度-1在角標中才是代表最後一個元素)
那麼可以看看我們遞迴的寫法了:
public static void main(String[] args) {
int[] arrays = {2, 3, 4, 5, 1, 5, 2, 9, 5, 6, 8, 3, 1};
System.out.println("公眾號:Java3y:" + findMax(arrays, 0, arrays.length - 1));
}
/**
* 遞迴,找出陣列最大的值
* @param arrays 陣列
* @param L 左邊界,第一個數
* @param R 右邊界,陣列的長度
* @return
*/
public static int findMax(int[] arrays, int L, int R) {
//如果該陣列只有一個數,那麼最大的就是該陣列第一個值了
if (L == R) {
return arrays[L];
} else {
int a = arrays[L];
int b = findMax(arrays, L + 1, R);//找出整體的最大值
if (a > b) {
return a;
} else {
return b;
}
}
}
三、氣泡排序遞迴寫法
在氣泡排序章節中給出了C語言的遞迴實現氣泡排序,那麼現在我們已經使用遞迴的基本思路了,我們使用Java來重寫一下看看:
氣泡排序:倆倆交換,在第一趟排序中能夠將最大值排到最後面,外層迴圈控制排序趟數,內層迴圈控制比較次數
以遞迴的思想來進行改造:
- 當第一趟排序後,我們可以將陣列最後一位(R)和陣列前面的數(L,R-1)進行切割,陣列前面的數(L,R-1)看成是一個整體,這個整體又是和我們的初始目的(找出最大值,與當前趟數的末尾處交換)是一樣的
- 遞迴出口:當只有一個元素時,即不用比較了:
L==R
public static void main(String[] args) {
int[] arrays = {2, 3, 4, 5, 1, 5, 2, 9, 5, 6, 8, 3, 1};
bubbleSort(arrays, 0, arrays.length - 1);
System.out.println("公眾號:Java3y:" + arrays);
}
public static void bubbleSort(int[] arrays, int L, int R) {
int temp;
//如果只有一個元素了,那什麼都不用幹
if (L == R) ;
else {
for (int i = L; i < R; i++) {
if (arrays[i] > arrays[i + 1]) {
temp = arrays[i];
arrays[i] = arrays[i + 1];
arrays[i + 1] = temp;
}
}
//第一趟排序後已經將最大值放到陣列最後面了
//接下來是排序"整體"的資料了
bubbleSort(arrays, L, R - 1);
}
}
四、斐波那契數列
接觸過C語言的同學很可能就知道什麼是費波納切數列了,因為往往做練習題的時候它就會出現,它也是遞迴的典型應用。
菲波那切數列長這個樣子:{1 1 2 3 5 8 13 21 34 55..... n }
數學好的同學可能很容易就找到規律了:前兩項之和等於第三項
例如:
1 + 1 = 2
2 + 3 = 5
13 + 21 = 34
如果讓我們求出第n項是多少,那麼我們就可以很簡單寫出對應的遞迴表示式了:Z = (n-2) + (n-1)
遞迴出口在本題目是需要有兩個的,因為它是前兩項加起來才得出第三項的值
同樣地,那麼我們的遞迴出口可以寫成這樣:if(n==1) retrun 1 if(n==2) return 2
下面就來看一下完整的程式碼吧:
public static void main(String[] args) {
int[] arrays = {1, 1, 2, 3, 5, 8, 13, 21};
//bubbleSort(arrays, 0, arrays.length - 1);
int fibonacci = fibonacci(10);
System.out.println("公眾號:Java3y:" + fibonacci);
}
public static int fibonacci(int n) {
if (n == 1) {
return 1;
} else if (n == 2) {
return 1;
} else {
return (fibonacci(n - 1) + fibonacci(n - 2));
}
}
五、漢諾塔演算法
玩漢諾塔的規則很簡單:
- 有三根柱子,原始裝滿大小不一的盤子的柱子我們稱為A,還有兩根空的柱子,我們分別稱為B和C(任選)
- 最終的目的就是將A柱子的盤子全部移到C柱子中
- 移動的時候有個規則:一次只能移動一個盤子,小的盤子不能在大的盤子上面(反過來:大的盤子不能在小的盤子上面)
我們下面就來玩一下:
- 只有一個盤子:
- 將A柱子的盤子直接移動到C柱子中
- 完成遊戲
- 只有兩個盤子:
- 將A柱子上的小盤子移動到B柱子中
- 將A柱子上的大盤子移動到C柱子中
- 最後將在B柱子的小盤子移動到C柱子大盤子中
- 完成遊戲
- 只有三個盤子:
- 將A柱子小的盤子移動到C柱子中
- 將A柱子上的中盤子移動到B柱子中
- 將C柱子小盤子移動到B柱子中盤子中
- 將A柱子的大盤子移動到C柱子中
- 將B柱子的小盤子移動到A柱子中
- 將B柱子的中盤子移動到C柱子中
- 最後將A柱子的小盤子移動到C柱子中
- 完成遊戲
…
從前三次玩法中我們就可以發現的規律:
- 想要將最大的盤子移動到C柱子,就必須將其餘的盤子移到B柱子處(藉助B柱子將最大盤子移動到C柱子中[除了最大盤子,將所有盤子移動到B柱子中])[遞迴表示式]
- 當C柱子有了最大盤子時,所有的盤子在B柱子。現在的目的就是藉助A柱子將B柱子的盤子都放到C柱子中(和上面是一樣的,已經發生遞迴了)
- 當只有一個盤子時,就可以直接移動到C柱子了(遞迴出口)
- A柱子稱之為起始柱子,B柱子稱之為中轉柱子,C柱子稱之為目標柱子
- 從上面的描述我們可以發現,起始柱子、中轉柱子它們的角色是會變的(A柱子開始是起始柱子,第二輪後就變成了中轉柱子了。B柱子開始是目標柱子,第二輪後就變成了起始柱子。總之,起始柱子、中轉柱子的角色是不停切換的)
簡單來說就分成三步:
- 把 n-1 號盤子移動到中轉柱子
- 把最大盤子從起點移到目標柱子
- 把中轉柱子的n-1號盤子也移到目標柱子
那麼就可以寫程式碼測試一下了(回看上面玩的過程):
public static void main(String[] args) {
int[] arrays = {1, 1, 2, 3, 5, 8, 13, 21};
//bubbleSort(arrays, 0, arrays.length - 1);
//int fibonacci = fibonacci(10);
hanoi(3, 'A', 'B', 'C');
System.out.println("公眾號:Java3y" );
}
/**
* 漢諾塔
* @param n n個盤子
* @param start 起始柱子
* @param transfer 中轉柱子
* @param target 目標柱子
*/
public static void hanoi(int n, char start, char transfer, char target) {
//只有一個盤子,直接搬到目標柱子
if (n == 1) {
System.out.println(start + "---->" + target);
} else {
//起始柱子藉助目標柱子將盤子都移動到中轉柱子中(除了最大的盤子)
hanoi(n - 1, start, target, transfer);
System.out.println(start + "---->" + target);
//中轉柱子藉助起始柱子將盤子都移動到目標柱子中
hanoi(n - 1, transfer, start, target);
}
}
我們來測試一下看寫得對不對:
參考資料:
六、總結
遞迴的確是一個比較難理解的東西,好幾次都把我繞進去了....
要使用遞迴首先要知道兩件事:
- 遞迴出口(終止遞迴的條件)
- 遞迴表示式(規律)
在遞迴中常常用”整體“的思想,在漢諾塔例子中也不例外:將最大盤的盤子看成1,上面的盤子看成一個整體。那麼我們在演算的時候就很清晰了:將”整體“搬到B柱子,將最大的盤子搬到C柱子,最後將B柱子的盤子搬到C柱子中
因為我們人腦無法演算那麼多的步驟,遞迴是用計算機來乾的,只要我們找到了遞迴表示式和遞迴出口就要相信計算機能幫我們搞掂。
在程式語言中,遞迴的本質是方法自己呼叫自己,只是引數不一樣罷了。
最後,我們來看一下如果是5個盤子,要運多少次才能運完:
PS:如果有更好的理解方法,或者我理解錯的地方大家可以在評論下留言一起交流哦!