什么是遞歸?
程序調用自身的編程技巧稱為遞歸( 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年提出的問題,是回溯算法的典型案例。
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(); } }