1. 程式人生 > 其它 >遞迴(包含迷宮問題)

遞迴(包含迷宮問題)

技術標籤:演算法與資料結構遞迴演算法演算法java

文章目錄

一、遞迴的概述

1.1 什麼是遞迴

遞迴就是方法自己呼叫自己,每次呼叫時傳入不同的變數。遞迴有助於程式設計者解決複雜的問題,同時可以讓程式碼變得簡潔。

簡單來說:遞迴,就是在方法執行的過程中呼叫自己。

構成遞迴需具備的條件:

  1. 子問題須與原始問題為同樣的事,且更為簡單;

  2. 不能無限制地呼叫本身,必須有個出口,化簡為非遞迴狀況處理。

1.2 遞迴要遵守的準則

在使用遞迴的過程中,需要遵守下面幾個重要準則:

  1. 執行一個方法時,就建立一個新的受保護的獨立空間(棧空間);
  2. 方法的區域性變數是獨立的,不會相互影響, 比如 n 變數;
  3. 如果方法中使用的是引用型別變數(比如陣列),就會共享該引用型別的資料;
  4. 遞迴必須向退出遞迴的條件逼近,否則就是無限遞迴,出現 StackOverflowError,死龜了;
  5. 當一個方法執行完畢,或者遇到 return,就會返回,且遵守誰呼叫,就將結果返回給誰,同時當方法執行完畢或
    者返回時,該方法也就執行完畢。

1.3 遞迴的執行機制

我們可以用兩個小案例來理解一下遞迴的執行機制。

1.3.1 案例一

需求:

使用遞迴來列印 n 次 “Hello World”。

分析:

要使用遞迴列印 n 次字串。首先根據遞迴的準則,要明確以下兩點:

  1. 遞迴退出的條件;
  2. 遞迴要逼近退出條件。

對於上面兩點,遞迴函式的初始引數一定是 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 遞迴的應用場景

遞迴可以解決許多實際性問題,主要可以概括為以下幾個方面:

  1. 各種數學問題,如 8 皇后問題、漢諾塔、階乘問題、迷宮問題、球和籃子問題等;
  2. 各種演算法中也會使用到遞迴,如快排、 歸併排序、二分查詢、分治演算法等;
  3. 欲使用棧解決的問題,使用遞迴來解決程式碼更加簡潔。

二、迷宮問題

2.1 問題描述

在這裡插入圖片描述

如上圖所示是一個簡單的迷宮,小球每次只能移動一個格子,且只能往上、下、左、右這四個方向移動。要求使用遞迴找出小球可以從起點走到終點的一條有效路徑。

2.2 思路分析

從上面的迷宮圖可以看出來,小球從起點走到終點可以有很多條路徑。那麼如何用程式碼實現呢?

  1. 建立一個二維陣列 arr[8][7]來代替迷宮;
  2. 約定陣列的值代表迷宮格子的狀態,如:
    • 值為 0 代表這個格子沒有走過;
    • 值為 1 代表這個格子是牆;
    • 值為 2 代表這個格子可以走通;
    • 值為 3 代表這個格子已經走過,但是走不通。
  3. 小球採取探測移動策略,即每次移動之前,先探測周圍的格子是否可以走通,再決定移動;
  4. 確定小球的探測移動順序,如按照 “上、左、下、右” 順序探測移動,還是 “上、下、左、右” 順序,亦或是 “左、上、右、下” 順序等等。每種探測順序的到的路徑一般是不相同的。
  5. 如果 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 上右下左策略

可以看到,使用兩種不同的策略,最終可以得到不同的路徑圖。