資料結構與演算法:遞迴

小高飛發表於2020-09-26

什麼是遞迴?

程式呼叫自身的程式設計技巧稱為遞迴( recursion)。遞迴做為一種演算法在程式設計語言中廣泛應用。 一個過程或函式在其定義或說明中有直接或間接呼叫自身的一種方法,它通常把一個大型複雜的問題層層轉化為一個與原問題相似的規模較小的問題來求解,遞迴策略只需少量的程式就可描述出解題過程所需要的多次重複計算,大大地減少了程式的程式碼量。遞迴的能力在於用有限的語句來定義物件的無限集合。一般來說,遞迴需要有邊界條件、遞迴前進段和遞迴返回段。當邊界條件不滿足時,遞迴前進;當邊界條件滿足時,遞迴返回。

如當你想知道什麼是遞迴時,在百度上搜尋遞迴,你會發現遞迴和棧有關,之後你為了瞭解棧是什麼又在百度上搜尋棧,而棧又和記憶體有關,所以你又搜尋了記憶體是什麼,直到了解了相關知識,再通過記憶體去了解棧,通過棧去了解遞迴。在這個過程中,百度就是遞迴方法,遞迴方法中的引數就是搜尋關鍵字,邊界條件就是知道和遞迴相關的所有知識點,遞迴前進段就是依次搜尋的過程,遞迴返回段就是了解完相關知識再回去學習。

資料結構與演算法:遞迴

簡單的遞迴程式碼實現

public class Recursion {
​
    public static void main(String[] args) {
        recursion(2);
    }
​
    public static void recursion(int n){
        if (n > 0){//當n>0繼續遞迴前進
            recursion(n-1);
        }
        //當n<0時,則停止遞迴前進,開始遞迴返回
        System.out.println(n);
    }
}

 

遞迴的實現原理

從上面程式碼中很容易看出,遞迴的前進是因為在if語句中呼叫了recursion方法,從而使得程式可以按照一定的規律遞迴前進,那遞迴中按照順序返回的遞迴返回是怎麼實現的呢?這時候我們就需要了解棧了。

在java虛擬機器中,棧是執行時的單位,每個方法在執行時都會建立一個棧幀(Stack Frame)用於儲存區域性變數表、運算元棧、動態連結、方法出口等資訊。每一個方法從呼叫直至執行結束,就對應著一個棧幀從虛擬機器棧中入棧到出棧的過程。當我們每呼叫一次recursion方法時,就會在棧中壓入一個棧幀,直到該方法執行完畢,就會按照棧的先入後出的規律出棧,依次執行呼叫遞迴方法recursion程式碼段的後面輸出程式碼,這就實現了遞迴返回。

資料結構與演算法:遞迴

 

遞迴的應用

尋找最短路徑

問題:有個7x7的迷宮,牆壁不能通過,只能從起點進入,終點逃出,求離開迷宮的最短路徑

注意:使用遞迴尋找最短路徑並不是最優的演算法,只是想說使用遞迴也能解決該問題

public class Labyrinth2 {

    // 用於儲存最短路徑,需為全域性變數
    static List<Position> shortest = new ArrayList<>();

    public static void main(String[] args) {
        //建立一個7x7的迷宮,1為牆壁,0為通道,2為起點, 有倆個終點map[6,2],map[6,5]
        int map[][] = {
                {1, 1, 1, 1, 1, 1, 1},
                {2, 0, 0, 0, 0, 0, 1},
                {1, 0, 0, 0, 0, 0, 1},
                {1, 1, 0, 0, 0, 0, 1},
                {1, 0, 0, 0, 1, 1, 1},
                {1, 0, 0, 0, 0, 0, 1},
                {1, 1, 0, 1, 1, 0, 1}
        };

        printMap(map);
        Position position = new Position(1, 0);
        Stack<Position> path = new Stack<>();

        findWay(map, position, path);
        System.out.println("迷宮通過路徑為:");
        printMap(map);
        System.out.printf("最短路徑是:");
        for (Position pos : shortest){
            System.out.printf("[%d, %d] ",pos.row, pos.cul);
        }
    }

    //判斷下一步是否是可行的通道
    public static boolean isClear(int[][] map, Position cur, Position next){
        //判斷下一步座標是否超出迷宮限制
        if (next.row>=0 && next.row<map.length && next.cul>=0 && next.cul<map[next.row].length){
            //判斷下一步通道是否為0,即可行通道,或下一步的值是否大於當前位置的值,即是之前路徑走過的通道但非回頭路
            if (map[next.row][next.cul] == 0 || map[cur.row][cur.cul] < map[next.row][next.cul]){
                return true;
            }
        }
        return false;
    }

    /**
     * 迷宮的定義:
     *  0為通道,1為牆壁,2為起點,終點在迷宮的最下方的通道,即map.length-1
     * 尋找最短路徑思路:
     *  1.先假定該通道是可行路徑上的一點,壓入存放可行路徑的棧path
     *  2.判斷是否已到達終點cur.row == map.length-1,如果已經到達終點,則判斷path和shortest內的路徑哪個短,
     *    如果path內的路徑短,則把path的值覆蓋掉shortest的值
     *  3.往四方探路:
     *    1)因為是要尋找最短路徑,所以得把所有通道路徑走一遍,即每次都要往四方(下-右-上-左)可行的通道都出發,
     *      所以不用if-else if語句,只使用if語句來判斷通道是否可行(條件為:isClear方法返回的boolean值)
     *    2)噹噹前方向是可行的通道時,要為該方向對應的下個通道賦值為(現在所處位置的值+1),來防止走回頭路(在isClear中有判斷是否為回頭路的條件)
     *    3)當 1)和2) 執行完後,再來遞迴(返回到流程1),同樣使用if語句,條件為遞迴方法findWay,
     *      噹噹前方向的通道有可行的路徑時(即該次遞迴方法最終返回的值為true),就返回true
     *  4.如果四方都不可行,就說明當前所在位置的通道是死路,把它彈出可行路徑棧path,
     *    返回false歸回到上次遞迴方法,如果該遞迴方法為第一個遞迴方法則結束返回結果回到main方法
     * @param map 迷宮地圖
     * @param cur 當前位置在迷宮中所處的座標
     * @param path 儲存離開迷宮路徑的每個通道座標
     * @return
     */
    public static boolean findWay(int[][] map, Position cur, Stack<Position> path){
        path.push(cur);
        if (cur.row == map.length-1){
            if (path.size() < shortest.size() || shortest.isEmpty()){
                shortest = (List<Position>)path.clone();
            }
        }

        //向下走
        Position next = new Position(cur.row, cur.cul);
        next.row = cur.row+1;
        if (isClear(map, cur, next)){ //判斷next座標是否是可行的通道
            map[next.row][next.cul] = map[cur.row][cur.cul]+1;//將cur座標的值+1賦給next座標
            if (findWay(map, next, path)) { //如果條件內的遞迴方法最終返回ture,則表示next的通道是可行路徑上的一點
                return true;
            }
        }

        //向右走
        next = new Position(cur.row, cur.cul);
        next.cul = cur.cul+1;
        if (isClear(map, cur, next)){
            map[next.row][next.cul] = map[cur.row][cur.cul]+1;
            if (findWay(map, next, path)) {
                return true;
            }
        }

        //向上走
        next = new Position(cur.row, cur.cul);
        next.row = cur.row-1;
        if (isClear(map, cur, next)){
            map[next.row][next.cul] = map[cur.row][cur.cul]+1;
            if (findWay(map, next, path)) {
                return true;
            }
        }

        //向左走
        next = new Position(cur.row, cur.cul);
        next.cul = cur.cul - 1;
        if (isClear(map, cur, next)){
            map[next.row][next.cul] =map[cur.row][cur.cul]+1;
            if (findWay(map, next, path)) {
                return true;
            }
        }

        path.pop();//所在座標的通道四方都不是可行路徑上的一點,即該通道也不是可行路徑上的一點,彈出可行路徑棧
        return false;//返回false,歸回到上次遞迴方法,如已歸回到第一次的遞迴方法,則返回結果到main方法
    }

    public static void printMap(int[][] map){
        for (int[] i : map){
            for (int item : i){
                System.out.printf("%d\t", item);
            }
            System.out.println();
        }
    }
}

//用於儲存位置在迷宮地圖中的座標
class Position{
     public int row;
     public int cul;

    public Position() {
    }

    public Position(int row, int cul) {
        this.row = row;
        this.cul = cul;
    }

    @Override
    public String toString() {
        return "Position{" +
                "row=" + row +
                ", cul=" + cul +
                '}';
    }
}

 

八皇后問題

八皇后問題(英文:Eight queens),是由國際西洋棋棋手馬克斯·貝瑟爾於1848年提出的問題,是回溯演算法的典型案例。

問題表述為:在8×8格的國際象棋上擺放8個皇后,使其不能互相攻擊,即任意兩個皇后都不能處於同一行、同一列或同一斜線上,問有多少種擺法。高斯認為有76種方案。1854年在柏林的象棋雜誌上不同的作者發表了40種不同的解,後來有人用圖論的方法解出92種結果。如果經過±90度、±180度旋轉,和對角線對稱變換的擺法看成一類,共有42類。計算機發明後,有多種計算機語言可以程式設計解決此問題。

public class Queen8 {

    static int max = 8;
    static int count = 0;//計算共有多少擺放的方式
    //儲存8個皇后擺放的位置,下標是棋盤行,值是棋盤列: int[row]=cul, row對應棋盤的行, cul對應棋盤的列
    static int[] array = new int[max];

    public static void main(String[] args) {

        Queen8 queen8 = new Queen8();
        queen8.putQueen(0);

        System.out.printf("共有 %d 種擺放方式\n", count);//92種
    }

    /**
     * 擺放皇后
     * @param row 皇后所在的行
     */
    public void putQueen(int row){
        //因為棋盤只有8行,所有當行數等於8(陣列是從0開始), 則說明8個皇后已擺放完成
        if (row == max){
            print();
            return;
        }

        for (int cul=0; cul<max; cul++){//遍歷該行的每一列
            //因為cul值是否根據迴圈變化的,所以只要沒有符合遊戲規則,就會重新賦值給array[row]
            array[row] = cul;//先假定row行的cul位置符合規則
            if (judge(row)){//判斷該位置是否符合規則
                putQueen(row+1);//符合則遞進到下一行
            }
            //不符合則繼續迴圈到下一列
        }
    }

    /**
     * 判斷皇后的擺放是否符號遊戲規則
     * 皇后擺放的規則:
     *  不能在同一行:因為是用一維陣列來對應皇后的座標,棋盤的行對應陣列的下標,
     *                每一行只有一個皇后,所以不用考慮同一行的問題
     *  不能在同一列:因為是用一維陣列array來儲存皇后所在的列,所在只要array內沒有相同的值就說明沒有皇后在同一列
     *  不能再同一斜線:當要和[row,cul]同一斜線時,只要[row-1,cul+1] 或[row-1,cul-1],
     *                可以看出 |row-(row-1)| = |cul-(cul+1)| = |cul-(cul-1)| = 1,
     *                即倆個位置的行和列相減後的差的絕對值都相等,所以只要該差的絕對值不相等就不在同一條斜線
     * @param row 皇后所在的行
     * @return
     */
    public static boolean judge(int row){
        for (int i=0; i<row; i++){ //遍歷在row行前的所有皇后
            //判斷是否在同一列 array[i] == array[row]
            //判斷是否在同一斜線 Math.abs(row-i) == Math.abs(array[row]-array[i])
            if (array[i] == array[row] || Math.abs(row-i) == Math.abs(array[row]-array[i])){
                return false; //如果在同一列或同一斜線,則不符合遊戲規則,返回false
            }
        }
        return true;
    }

    //列印棋盤內皇后的擺放位置,根據行排序,如第一個數是第一行皇后所在的列
    public static void print(){
        count++;
        for (int i : array){
            System.out.print(i+" ");
        }
        System.out.println();
    }
}

 

相關文章