5.遞歸

目錄

1.遞歸的概念
2.遞歸需要遵守的重要規則
3.遞歸的應用
4.迷宮問題
 4.1 問題簡介
 4.2 解決方案
  4.2.1 求解一條可行路徑
  4.2.2 求解最短路徑
  4.2.3 補充
5.八皇后問題

1.遞歸的概念

簡單的說:遞歸就是方法自己調用自己,每次調用時傳入不同的變量。遞歸有助於編程者解決複雜的問題,同時可以讓代碼變得簡潔。

例如下面的函數就是遞歸調用,傳參4時的打印結果爲2 3 4:

public static void test(int n) { 
	if (n > 2) {
		test(n - 1);
	} 
	System.out.println(n);
}

2.遞歸需要遵守的重要規則

1.執行一個方法時,就創建一個新的受保護的獨立空間(棧空間);
2.方法的局部變量是獨立的,不會相互影響, 比如上述示例n變量;
3.如果方法中使用的是引用類型變量(比如數組),就會共享該引用類型的數據;
4.遞歸必須向退出遞歸的條件逼近,否則就是無限遞歸,會出現棧溢出錯誤。
5.當一個方法執行完畢,或者遇到return,就會返回,遵守誰調用,就將結果返回給誰,同時當方法執行完畢或者返回時,該方法也就執行完畢。

3.遞歸的應用

1.各種數學問題如: 8皇后問題 , 漢諾塔, 階乘問題, 迷宮問題, 球和籃子的問題等;
2.各種算法中也會使用到遞歸,比如快排,歸併排序,二分查找,分治算法等;
3.需要用棧解決的問題轉換爲用遞歸解決,能使代碼比較簡潔;

4.迷宮問題

4.1 問題簡介

給定一個迷宮,指明起點和終點,找出從起點出發到終點的有效可行路徑,就是迷宮問題(maze problem)。

迷宮可以以二維數組來存儲表示。這裏距離規定:0表示通路,1表示障礙。規定移動可以從上、下、左、右四方方向移動。座標以行和列表示,均從0開始,給定起點(1,1)和終點(5,3),迷宮地圖表示如下:

int map[5][5]={
	{1,1,1,1,1,1,1},
    {1,0,0,0,0,0,1},
    {1,0,1,0,1,0,1},
    {1,0,1,1,0,0,1},
    {1,0,1,1,0,1,1},
    {1,0,0,0,0,0,1},
    {1,1,1,1,1,1,1}
};

那麼上述的迷宮就有兩條可行的路徑,分別爲:

可見,迷宮可行路徑有可能是多條,且路徑長度可能不一。

4.2 解決方案

迷宮問題的求解可以抽象爲連通圖的遍歷,因此可以用深度優先搜索(DFS)加回溯的方法,利用棧的數據結構來實現。在上述遞歸的應用中提到遞歸可以用於優化需要用棧來解決的問題,因此下述將先用棧來實現,便於理解,再用遞歸來優化。

4.2.1 求解一條可行路徑

(1)給定起點和終點,判斷二者的合法性,如果不合法,返回;
(2)如果起點和終點合法,將起點入棧;
(3)取棧頂元素,求其鄰接是否存在有未被訪問的無障礙結點。如果有,記其爲已訪問,併入棧。如果沒有則回溯上一結點,具體做法是將當前棧頂元素出棧。其中,求鄰接無障礙結點的順序可任意,示例採用順時針:上、右、下、左的順序訪問,採用不同的訪問順序得到的結果可能不同。
(4)重複步驟(3),直到棧頂元素等於終點(找到第一條可行路徑)或者棧空(沒有找到可行路徑)。

這裏規定:將二維數組迷宮地圖中訪問了的地方標記爲2,已經訪問了且爲死路的標記爲3

package stack;

import java.util.Stack;

class Point{
    //行與列
    public int row;
    public int col;

    public Point(int row, int col){
        this.row = row;
        this.col = col;
    }

    public boolean equal(Point point){
        return point.row == this.row && point.col == this.col;
    }

    @Override
    public String toString() {
        return "Point(" + row + ", " + col + ")";
    }
};

public class Maze {
    /**
     * 初始化迷宮地圖
     */
    private static int[][] map = new int[][]{
            {1,1,1,1,1,1,1},
            {1,0,0,0,0,0,1},
            {1,0,1,0,1,0,1},
            {1,0,1,1,0,0,1},
            {1,0,1,1,0,1,1},
            {1,0,0,0,0,0,1},
            {1,1,1,1,1,1,1}
    };
    //起點
    private static Point startP = new Point(1,1);
    //終點
    private static Point endP = new Point(5,3);
    //存放路徑
    private static Stack<Point> pointStack = new Stack<>();

    public static void main(String[] args) {
        System.out.println("初始迷宮地圖:");
        showMap();
        //查找可行路徑
        mazePath();
        System.out.println("查找一條出迷宮的可行路徑後的迷宮地圖:");
        showMap();
        System.out.println("路徑查找結果:");
        //沒有找到可行解
        if(pointStack.empty()==true) {
            System.out.println("沒有找到出口!");
        }
        else {
            //逆向輸出棧中節點即爲路徑
            Stack<Point> tempPointStack = new Stack<>();
            while(pointStack.empty()==false) {
                tempPointStack.push(pointStack.pop());
            }
            while (!tempPointStack.empty()) {
                Point point = tempPoint1Stack.pop();
            	System.out.printf("(%d,%d) ",point.row,point.col);
            }
        }

    }

    /**
     * 深度優先搜索(DFS)加回溯查找一條可行路徑,順序:上、右、下、左
     */
    public static void mazePath(){
        //起點和終點必須爲無障礙結點,否則錯誤
        if(map[startP.row][startP.col] == 1 || map[endP.row][endP.col] == 1) {
            throw new RuntimeException("起點或終點不能爲障礙點!") ;
        }
        //將起點入棧
        pointStack.push(startP);
        map[startP.row][startP.col] = 2;
        //棧不空並且棧頂元素不爲結束節點則查找路徑
        while(!pointStack.empty() && !endP.equal(pointStack.peek())){
            Point point = pointStack.peek();
            if (map[point.row-1][point.col]==0) {//上節點滿足條件
                //入棧並設置訪問標誌爲2
                pointStack.add(new Point(point.row-1,point.col));
                map[point.row-1][point.col] = 2;
            }else if (map[point.row][point.col+1]==0) {//右節點滿足條件
                //入棧並設置訪問標誌爲2
                pointStack.add(new Point(point.row,point.col+1));
                map[point.row][point.col+1] = 2;
            }else if (map[point.row+1][point.col]==0) {//下節點滿足條件
                //入棧並設置訪問標誌爲2
                pointStack.add(new Point(point.row+1,point.col));
                map[point.row+1][point.col] = 2;
            }else if (map[point.row][point.col-1]==0) {//左節點滿足條件
                //入棧並設置訪問標誌爲2
                pointStack.add(new Point(point.row,point.col-1));
                map[point.row][point.col-1] = 2;
            }else {
                //回溯到上一個節點並訪問標誌爲3
                pointStack.pop();
                map[point.row][point.col] = 3;
            }
        }
    }

    /**
     * 打印迷宮地圖
     */
    public static void showMap(){
        for (int i = 0; i < 7; i++) {
            for (int j = 0; j < 7; j++) {
                System.out.print(map[i][j]+"  ");
            }
            System.out.println();
        }
    }
}

運行結果:
在這裏插入圖片描述
將上述用棧實現的算法改爲用遞歸實現:

package stack;

import java.util.Stack;

class Point{
    //行與列
    public int row;
    public int col;

    public Point(int row, int col){
        this.row = row;
        this.col = col;
    }

    public boolean equal(Point point){
        return point.row == this.row && point.col == this.col;
    }

    @Override
    public String toString() {
        return "(" + row + ", " + col + ")";
    }
}

public class RecursionMaze {
    /**
     * 初始化迷宮地圖
     */
    private static int[][] map = new int[][]{
            {1,1,1,1,1,1,1},
            {1,0,0,0,0,0,1},
            {1,0,1,0,1,0,1},
            {1,0,1,1,0,0,1},
            {1,0,1,1,0,1,1},
            {1,0,0,0,0,0,1},
            {1,1,1,1,1,1,1}
    };
    //起點
    private static Point startP = new Point(1,1);
    //終點
    private static Point endP = new Point(5,3);

    public static void main(String[] args) {
        System.out.println("初始迷宮地圖:");
        showMap();
        //查找可行路徑
        //起點和終點必須爲無障礙結點,否則錯誤
        if(map[startP.row][startP.col] == 1 || map[endP.row][endP.col] == 1) {
            throw new RuntimeException("起點或終點不能爲障礙點!") ;
        }
        mazePath(startP);
        System.out.println("查找一條出迷宮的可行路徑後的迷宮地圖:");
        showMap();
        System.out.println("路徑查找結果:");
        for (int i = 0; i < 7; i++) {
            for (int j = 0; j < 7; j++) {
                if (map[i][j]==2){
                    System.out.printf("(%d,%d) ",i,j);
                }
            }
        }
    }

    /**
     * 深度優先搜索(DFS)加回溯查找一條可行路徑,順序:上、右、下、左
     */
    public static boolean mazePath(Point point){
        if (endP.equal(point)){//找到終點
            map[point.row][point.col] = 2;
            return true;
        }else {
            if(map[point.row][point.col] == 0) { //如果當前這個點還沒有走過
                map[point.row][point.col] = 2;
                if (mazePath(new Point(point.row-1,point.col))) { // 向上走
                    return true;
                } else if (mazePath(new Point(point.row,point.col+1))) { // 向右
                    return true;
                } else if (mazePath(new Point(point.row+1,point.col))){ // 向下走
                    return true;
                } else if (mazePath(new Point(point.row,point.col-1))){ // 向左走
                    return true;
                }else {//說明沒有地方可以走了,該處是死路,標記爲 3;
                    map[point.row][point.col] = 3;
                    return false;
                }
            }else {// 如果不爲0 ,則可能是 1 2 3,牆體、走過的、死路都應該回溯
                return false;
            }
        }
    }

    /**
     * 打印迷宮地圖
     */
    public static void showMap(){
        for (int i = 0; i < 7; i++) {
            for (int j = 0; j < 7; j++) {
                System.out.print("  "+map[i][j]);
            }
            System.out.println();
        }
    }
}

通過比較上述用棧實現和用遞歸的實現容易看出,遞歸的代碼結構明顯更簡潔,但更難理解。

另外,一般的,當搜索深度較小、問題遞歸方式比較明顯時,用遞歸方法設計好,它可以使得程序結構更簡捷易懂。當數據量較大時,由於系統堆棧容量的限制,遞歸容易產生溢出,用非遞歸方法設計比較好。

4.2.2 求解最短路徑

上面的方法只在乎找到一條可行路徑,而並不關心這條路徑是不是最短的,有時在實際情況下我們需要查找的是最短路徑。因此,根據上面的方法我們可以進行改進,求出迷宮的最短路徑。具體做法如下:

(1)讓已經訪問過的結點可以再次被訪問,具體做法是將map中標記改爲當前結點到起點的距離,作爲當前結點的權值。即從起點開始出發,向四個方向查找,每走一步,把走過的點的值+1(爲了避免和權值衝突,將牆改爲-1,起點的距離權值爲1);
(2)尋找棧頂元素的下一個可訪問的相鄰結點,條件就是棧頂元素的權值加1必須小於下一個節點的權值或下一個節點未被訪問(牆不能走,未被訪問的結點權值爲0);
(3)如果訪問到終點,記錄當前最短的路徑。如果不是,則繼續尋找下一個結點;
(4)重複步驟(2)和(3)直到棧空(迷宮中所有符合條件的結點均被訪問)。

package stack;
import java.util.Stack;
import java.util.function.UnaryOperator;

class Point{
    //行與列
    public int row;
    public int col;

    public Point(int row, int col){
        this.row = row;
        this.col = col;
    }

    public boolean equal(Point point){
        return point.row == this.row && point.col == this.col;
    }

    @Override
    public String toString() {
        return "(" + row + ", " + col + ")";
    }
};

public class ShortestPathMaze {
    /**
     * 初始化迷宮地圖
     */
    private static int[][] map = new int[][]{
            {-1,-1,-1,-1,-1,-1,-1},
            {-1, 0, 0, 0, 0, 0,-1},
            {-1, 0,-1, 0,-1, 0,-1},
            {-1, 0,-1,-1, 0, 0,-1},
            {-1, 0,-1,-1, 0,-1,-1},
            {-1, 0, 0, 0, 0, 0,-1},
            {-1,-1,-1,-1,-1,-1,-1}
    };
    //起點
    private static Point startP = new Point(1,1);
    //終點
    private static Point endP = new Point(5,3);
    //存放訪問路徑
    private static Stack<Point> pointStack = new Stack<>();
    //存放最短路徑
    private static Stack<Point> shortestPath = new Stack<>();

    public static void main(String[] args) {
        System.out.println("初始迷宮地圖:");
        showMap();
        //查找可行路徑
        mazePath();
        System.out.println("查找一條出迷宮的可行路徑後的迷宮地圖:");
        showMap();
        System.out.println("路徑查找結果:");
        //逆序輸出
        //逆向輸出棧中節點即爲路徑
        Stack<Point> tempPoint1Stack = new Stack<>();
        while(shortestPath.empty()==false) {
            tempPoint1Stack.push(shortestPath.pop());
        }
        while (!tempPoint1Stack.empty()) {
            Point point = tempPoint1Stack.pop();
            System.out.printf("(%d,%d) ",point.row,point.col);
        }
    }

    /**
     * 深度優先搜索(DFS)加回溯查找一條可行路徑,順序:上、右、下、左
     */
    public static void mazePath(){
        //起點和終點必須爲無障礙結點,否則錯誤
        if(map[startP.row][startP.col] == -1 || map[endP.row][endP.col] == -1) {
            throw new RuntimeException("起點或終點不能爲障礙點!") ;
        }
        //將起點入棧
        pointStack.push(startP);
        map[startP.row][startP.col] = 1;
        //記錄當前找到終點路徑的長度
        int pathLength = 0;
        //棧不空則查找路徑
        while(!pointStack.empty()){
            Point point = pointStack.peek();
            Point nextPoint = null;
            if ((map[point.row-1][point.col]==0 || map[point.row-1][point.col]>map[point.row][point.col]+1) && map[point.row-1][point.col]!=-1) {
                //上節點滿足條件
                nextPoint = new Point(point.row-1,point.col);
            }else if ((map[point.row][point.col+1]==0 || map[point.row][point.col+1]>map[point.row][point.col]+1) && map[point.row][point.col+1]!=-1) {
                //右節點滿足條件
                nextPoint = new Point(point.row,point.col+1);
            }else if ((map[point.row+1][point.col]==0 || map[point.row+1][point.col]>map[point.row][point.col]+1) && map[point.row+1][point.col]!=-1) {
                //下節點滿足條件
                nextPoint = new Point(point.row+1,point.col);
            }else if ((map[point.row][point.col-1]==0 || map[point.row][point.col-1]>map[point.row][point.col]+1) && map[point.row][point.col-1]!=-1) {
                //左節點滿足條件
                nextPoint = new Point(point.row,point.col-1);
            }else {//沒有找到符合條件的節點,回溯到上一節點
                pointStack.pop();
                continue;
            }
            pointStack.push(nextPoint);
            map[nextPoint.row][nextPoint.col] = map[point.row][point.col]+1;
            //找到了終點即找到了一條路徑,記錄該路徑,回溯繼續查找其他可行路徑,
            //若當前爲最短路徑,則後續由於權值,都無法到達終點,都會回溯直到棧空;若不爲最短路徑,則會被找到的更短路徑替換
            if (endP.equal(nextPoint)){
                shortestPath.clear();
                shortestPath.addAll(pointStack);
                //終點出棧,由於之前記錄了終點到起點的距離,所以後續將回溯直到找到另一條路
                pointStack.pop();
            }
        }
    }

    /**
     * 打印迷宮地圖
     */
    public static void showMap(){
        for (int i = 0; i < 7; i++) {
            for (int j = 0; j < 7; j++) {
                System.out.print(map[i][j]+"\t");
            }
            System.out.println();
        }
    }
}

4.2.3 補充

另外還有一種方法,廣度優先搜索(BFS),該方法利用隊列,從起點開始向每一個可行方向同時前進,將節點入隊,這樣最先找到終點的那條路一定是最短路徑。爲了準確找到路徑,可以用一個和迷宮地圖一樣大的節點二位數組,存儲該節點的前驅節點,這樣找到終點後便可以通過終點沿着最短路徑回到起點,找到路徑。

5.八皇后問題

八皇后問題,是一個古老而著名的問題,是回溯算法的典型案例。該問題是國際西洋棋棋手馬克斯·貝瑟爾於1848 年提出:在 8×8 格的國際象棋上擺放八個皇后,使其不能互相攻擊,即:任意兩個皇后都不能處於同一行、同一列或同一斜線上,問有多少種擺法(92)。
在這裏插入圖片描述

八皇后問題算法思路分析:

(1)第一個皇后先放第一行第一列;
(2)第二個皇后放在第二行第一列、然後判斷是否 OK, 如果不 OK,繼續放在第二列、第三列…依次把所有列都放完,找到一個合適的列;
(3)繼續第三個皇后,還是第一列、第二列……直到第 8 個皇后也能放在一個不衝突的位置,算是找到了一個正確解;
(4)當得到一個正確解時,在棧回退到上一個棧時,就會開始回溯,即將第一個皇后,放到第一列的所有正確解, 全部得到;
(5)然後回頭繼續第一個皇后放第二列,後面繼續循環執行 (1),(2),(3),(4) 的步驟,直到找到所有擺法。

說明:

理論上應該創建一個二維數組來表示棋盤,但是實際上可以通過算法,用一個一維數組即可解決問題。如:arr[8] =
{0 , 4, 7, 5, 2, 6, 1, 3} //對應 arr 下標 表示第幾行,即第幾個皇后,arr[i] = val , val 表示第 i+1 個皇后,放在第 i+1 行的第 val+1 列。

代碼實現:

package stack;

public class Queen {
    //表示皇后數量
    private int max = 0;
    //定義數組 array, 保存皇后放置位置的結果,比如 arr = {0 , 4, 7, 5, 2, 6, 1, 3}
    private int[] array;
    //一共有多少種解法
    private int count = 0;

    public Queen(int max){
        this.max = max;
        array = new int[max];
    }

    public static void main(String[] args) {
        Queen queen8 = new Queen(8);
        //放置第一個皇后,後續遞歸放置
        queen8.putQueen(0);
        System.out.printf("一共有%d 解法", queen8.count);
    }

    /**
     * 放置第n個皇后,即放第幾行,遞歸實現
     * @param n
     */
    public void putQueen(int n){
        if (n == max){ //放置完最後一個皇后即結束,回溯
            printArray();
            count++;//找到一種放置方法,記錄
            return;
        }
        //查找該皇后可以放置的列
        for(int i = 0; i < max; i++) {
            //先把當前這個皇后 n ,  放到該行的第 i 列
            array[n] = i;
            //判斷當放置第 n 個皇后到 i 列時,是否衝突
            if(check(n)) { //  不衝突
                //接着放 n+1 個皇后,即開始遞歸
                putQueen(n+1);
            }
            //如果衝突,就繼續執行 array[n] = i; 即將第 n 個皇后,後移一列
        }
    }

    /**
     * 判斷當前第n個皇后的放置的列是否符合要求
     * @param n
     * @return
     */
    public boolean check(int n){
        for (int i = 0; i < n; i++) {
            //array[i] == array[n]	表示判斷 第 n 個皇后是否和前面的 n-1 個皇后在同一列
            //Math.abs(n-i) == Math.abs(array[n] - array[i]) 表示判斷第 n 個皇后是否和第 i 皇后是否在同一斜線。行差等於列差即在同一斜線上
            if(array[i] == array[n] || Math.abs(n-i) == Math.abs(array[n] - array[i]) ) {
                return false;
            }
        }
        return true;
    }

    /**
     * 輸出當前這種皇后的詳細擺放
     */
    private void printArray() {
        for (int i = 0; i < array.length; i++) {
            System.out.print(array[i] + " ");
        }
        System.out.println();
    }

}

運行結果:
在這裏插入圖片描述
爲了測試查找到的92種解法是否是正確的,可以去八皇后的在線小遊戲 隨便選運行結果中的一種進行測試。
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章