遞迴(包含迷宮問題)
文章目錄
一、遞迴的概述
1.1 什麼是遞迴
遞迴就是方法自己呼叫自己,每次呼叫時傳入不同的變數。遞迴有助於程式設計者解決複雜的問題,同時可以讓程式碼變得簡潔。
簡單來說:遞迴,就是在方法執行的過程中呼叫自己。
構成遞迴需具備的條件:
-
子問題須與原始問題為同樣的事,且更為簡單;
-
不能無限制地呼叫本身,必須有個出口,化簡為非遞迴狀況處理。
1.2 遞迴要遵守的準則
在使用遞迴的過程中,需要遵守下面幾個重要準則:
- 執行一個方法時,就建立一個新的受保護的獨立空間(棧空間);
- 方法的區域性變數是獨立的,不會相互影響, 比如 n 變數;
- 如果方法中使用的是引用型別變數(比如陣列),就會共享該引用型別的資料;
- 遞迴必須向退出遞迴的條件逼近,否則就是無限遞迴,出現 StackOverflowError,死龜了;
- 當一個方法執行完畢,或者遇到
return
,就會返回,且遵守誰呼叫,就將結果返回給誰,同時當方法執行完畢或
者返回時,該方法也就執行完畢。
1.3 遞迴的執行機制
我們可以用兩個小案例來理解一下遞迴的執行機制。
1.3.1 案例一
需求:
使用遞迴來列印 n 次 “Hello World”。
分析:
要使用遞迴列印 n 次字串。首先根據遞迴的準則,要明確以下兩點:
- 遞迴退出的條件;
- 遞迴要逼近退出條件。
對於上面兩點,遞迴函式的初始引數一定是 n,如果要想列印 n 次,也就是要執行 n 次遞迴函式。假設每次遞迴時傳遞的引數是 n-1,那麼遞迴的退出條件則是 n <= 1,反之,函式遞迴呼叫的條件則是 n > 1。
程式碼實現:
public class No1_Recursion_BasicDemo {
public static void main(String[] args) {
// 遞迴列印
printHello(3);
}
/**
* @Description 遞迴列印 Hello world
* @Param [n] 列印次數
*/
public static void printHello(int n){
if (n > 1){
printHello(n-1);
}
System.out.println("Hello World!");
}
}
執行結果:
分析:
從上面的執行結果可以看出來,遞迴函式的初始引數為 n 時,將遞迴列印 n 次指定的字串,符合題目需求。
那麼這個遞迴函式是如何執行的呢?
下面將使用一個動圖來描述當主函式呼叫 printHello(3)
時這個遞迴函式的執行過程:
1.3.2 案例二
需求:
使用遞迴來求解 n 的階乘。
程式碼實現:
public class No1_Recursion_BasicDemo {
public static void main(String[] args) {
// 計算 4 的階乘
int factorial = getFactorial(4);
System.out.println(factorial);
}
/**
* @Description 計算 n 的階乘
*/
public static int getFactorial(int n){
if (n == 1){
return 1;
}else{
return n * getFactorial(n-1);
}
}
}
執行結果:
分析:
當主函式呼叫了 getFactorial(4)
時,這個遞迴的執行過程可以這麼理解:
getFactorial(4)
=> 4 * getFactorial(3)
=> 4 * (3 * getFactorial(2))
=> 4 * (3 * (2 * getFactorial(1)))
=> 4 * (3 * (2 * 1))
=> 4 * (3 * 2)
=> 4 * 6
=> 24
1.4 遞迴的應用場景
遞迴可以解決許多實際性問題,主要可以概括為以下幾個方面:
- 各種數學問題,如 8 皇后問題、漢諾塔、階乘問題、迷宮問題、球和籃子問題等;
- 各種演算法中也會使用到遞迴,如快排、 歸併排序、二分查詢、分治演算法等;
- 欲使用棧解決的問題,使用遞迴來解決程式碼更加簡潔。
二、迷宮問題
2.1 問題描述
如上圖所示是一個簡單的迷宮,小球每次只能移動一個格子,且只能往上、下、左、右這四個方向移動。要求使用遞迴找出小球可以從起點走到終點的一條有效路徑。
2.2 思路分析
從上面的迷宮圖可以看出來,小球從起點走到終點可以有很多條路徑。那麼如何用程式碼實現呢?
- 建立一個二維陣列
arr[8][7]
來代替迷宮; - 約定陣列的值代表迷宮格子的狀態,如:
- 值為 0 代表這個格子沒有走過;
- 值為 1 代表這個格子是牆;
- 值為 2 代表這個格子可以走通;
- 值為 3 代表這個格子已經走過,但是走不通。
- 小球採取探測移動策略,即每次移動之前,先探測周圍的格子是否可以走通,再決定移動;
- 確定小球的探測移動順序,如按照 “上、左、下、右” 順序探測移動,還是 “上、下、左、右” 順序,亦或是 “左、上、右、下” 順序等等。每種探測順序的到的路徑一般是不相同的。
- 如果
arr[6][5] == 2
,說明小球走到了終點。那麼在此時的陣列中,值為 2 的索引組成的路徑即為有效路徑。
2.3 程式碼實現
迷宮問題的程式碼實現如下,採用了兩種策略,分別是: “下、右、上、左” 探測移動策略、“上、右、下、左” 探測移動策略:
public class No2_Recursion_Maze {
private static final int row = 8;
private static final int col = 7;
public static void main(String[] args) {
// 建立一個 8 行 7 列的迷宮
int[][] maze = new int[row][col];
// 初始化迷宮
setMaze(maze);
showMaze(maze);
System.out.println("=========【下右上左】遞迴走迷宮==========");
setWay(maze, 1, 1);
showMaze(maze);
// 初始化迷宮
maze = new int[row][col];
setMaze(maze);
System.out.println("=========【上右下左】遞迴走迷宮==========");
setWay2(maze, 1, 1);
showMaze(maze);
}
/**
* @Description 小球從座標 maze[1][1] 開始,採用 下、右、上、左 策略來走迷宮
* @Param [maze, i, j] maze:迷宮 i:當前所處行 j:當前所處列
* @return boolean
*/
public static boolean setWay(int[][] maze, int i, int j){
// 1. 遞迴結束條件:當迷宮出口被置為 2 時,也即說明小球走到了這個格子
if (maze[6][5] == 2){
return true;
}else{
// 2. 如果不滿足遞迴結束條件,繼續遞迴
if (maze[i][j] == 0){ // 如果沒走過這個格子
maze[i][j] = 2; // 假設這個格子之後能走通
if (setWay(maze, i+1, j)){ // 先向下走
return true;
}else if (setWay(maze, i, j+1)){ // 如果向下走不通,就向右走
return true;
}else if (setWay(maze, i-1, j)){ // 如果向右走不通,就向上走
return true;
}else if (setWay(maze, i, j-1)){ // 如果向上走不通,就向左走
return true;
}else{ // 如果上下左右都走不通,就說明這個格子是死路
maze[i][j] = 3;
return false;
}
}else{ // 如果這個格子是 1/2/3,由於這裡沒有回溯演算法,就說明走不出去了
return false;
}
}
}
/**
* @Description 小球從 maze[1][1] 開始,採用上、右、下、左策略走迷宮
* @Param [maze, i, j]
* @return boolean
*/
public static boolean setWay2(int[][] maze, int i, int j){
// 1. 遞迴結束條件:當迷宮出口被置為 2 時,也即說明小球走到了這個格子
if (maze[6][5] == 2){
return true;
}else {
// 2. 如果不滿足遞迴結束條件,繼續遞迴
if (maze[i][j] == 0) { // 如果沒走過這個格子
maze[i][j] = 2; // 假設這個格子之後能走通
if (setWay2(maze, i-1, j)) { // 先向上走
return true;
} else if (setWay2(maze, i, j+1)) { // 如果向上走不通,就向右走
return true;
} else if (setWay2(maze, i+1, j)) { // 如果向右不通,就向下走
return true;
} else if (setWay2(maze, i, j-1)) { // 如果向下走不通,就向左走
return true;
} else { // 如果上下左右都走不通,就說明這個格子是死路
maze[i][j] = 3;
return false;
}
} else { // 如果這個格子是 1/2/3,由於這裡沒有回溯演算法,就說明走不出去了
return false;
}
}
}
/**
* @Description 初始化迷宮
* @Param [maze]
*/
public static void setMaze(int[][] maze){
// 第一行和第八行都是 1
for (int i=0; i<col; i++){
maze[0][i] = 1;
maze[7][i] = 1;
}
// 第一列和第七列也都是 1
for (int i=0; i<row; i++){
maze[i][0] = 1;
maze[i][6] = 1;
}
// 第四行第二列和第四行第三列都是 1
maze[3][1] = 1;
maze[3][2] = 1;
}
/**
* @Description 顯示迷宮狀態
* @Param [maze]
*/
public static void showMaze(int[][] maze){
// 列印初始迷宮
for (int i=0; i<row; i++){
for (int j=0; j<col; j++){
System.out.print(maze[i][j]);
}
System.out.println();
}
}
}
最終的執行結果如下:
圖1 下右上左策略 |
圖2 上右下左策略 |
可以看到,使用兩種不同的策略,最終可以得到不同的路徑圖。