回顾知识点
递归概念:递归就是方法自己调用自己,每次调用时传入不同的变量。递归有助于编程者解决复杂的问题,同时可以让代码变得简洁。
递归调用机制:
1)打印问题
2)阶乘问题
// 打印问题 private static void test(int n){ if(n > 1){ test(n - 1); // 到1为止 } System.out.println("n = " + n); } // 阶乘问题 private static int factorial(int n){ if(n == 1){ // 递归停止条件 return 1; }else{ return n * factorial(n - 1); } }
注意点:递归方法一定要有递归停止条件,或递归限制条件。
递归的应用场景:
1)各种数学问题:八皇后问题,汉诺塔,阶乘问题,迷宫问题,球和篮子问题。
2)各种算法中也会使用到递归,比如快排,归并排序,二分查找,分治算法等。
3)将用栈解决的问题-->递归代码比较简洁。
递归需要遵守的重要规则:
1)执行一个方法时,就创建一个新的受保护的独立空间(栈空间);
2)方法的局部变量是独立的,不会相互影响;(如果方法中使用的是引用类型变量,就会共享该引用类型的数据,因为对象new出来放在了堆里)
3)递归必须向退出递归的条件逼近,否则就是无限递归;
4)当一个方法执行完毕,或者遇到return,返回,遵守谁调用就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕。
什么是回溯法?
回溯法的基本思想:对一个包括有很多结点,每一个结点有若干个搜索分支的问题,把原问题分解为对若干个子问题求解的算法。
我们简单分析一下这句话,其实就是当搜索到某个结点,发现无法再继续搜索下去的时候,就让搜索过程回溯(也称退回)到该结点的上一个结点,继续搜索这个结点的其他尚未搜索过的分支,然后一遍一遍重复这个步骤,直到搜索到问题的解或搜索完了全部可搜索分支没有解存在为止。
1.迷宫问题
public static void main(String[] args) { // 先创建一个二维数组,模拟迷宫 int[][] map = new int[8][7]; // 8行7列 0-7,0-6 // 使用1表示墙 // 将上下两行置为墙 for (int i = 0; i < 7; i++) { map[0][i] = 1; map[7][i] = 1; } // 将左右两列置为墙 for (int i = 0; i < 8; i++) { map[i][0] = 1; map[i][6] = 1; } // 设置挡板 map[3][1] = 1; map[3][2] = 1; //输出这个地图 System.out.println("地图情况"); for (int i = 0; i < 8; i++) { for (int j = 0; j < 7; j++) { System.out.print(map[i][j] + " "); } System.out.println(); // 换行 } // 使用递归回溯给小球走路 getWay(map,1,1); // 输出新的地图,表示小球走过,并标识过的地图 System.out.println("小球走过的地图:"); for (int i = 0; i < 8; i++) { for (int j = 0; j < 7; j++) { System.out.print(map[i][j] + " "); } System.out.println(); // 换行 } } // 使用递归回溯来给小球找路 /* 约定:当map[i][j]为0表示该带你没有走过 为1表示墙,为2表示通路可以走 为3表示该位置已经走过,但走不通 走迷宫时的策略:下-->右-->上-->左 */ // 输入/** ,点击“Enter”,自动根据参数和返回值生成注释模板 /** * * @param map 表示地图 * @param i 从哪个位置开始找(行) * @param j 从哪个位置开始找(列) * @return 如果找到通路,返回true,否则,返回false */ public static boolean getWay(int[][] map,int i,int j){ // 终点定为[6][5] // 如果终点被标记为2,则找到通路 if(map[6][5] == 2){ // 递归停止条件 return true; } else{ // 还没有找到 if(map[i][j] == 0){ // 如果当前这个点还没走过 // 按策略下-->右-->上-->左 map[i][j] = 2; // 先假定这个点能走通 /*注意此处虽然是优先级等同的else if,但实质Java代码执行判断的时候 是按写的顺序来的,走了上面的if或者else if走不通才来下一个,所以策略没问题*/ // 向下走,行+1,列不变 if(getWay(map,i+1,j)){ // 走得通 return true; // true表示可以往这个没走过的点走 // 暂定是通路的一个点,如果不是,回溯的时候会将其置3和false的 } else if(getWay(map,i,j+1)){ // 往下走不通,走右 return true; } else if(getWay(map,i-1,j)){ // 向右走不通,走上 return true; } else if(getWay(map,i,j-1)){ // 向上走不通,走左 return true; } else{ // 上下左右都走不通 map[i][j] = 3; // 说明该点是走不通的,是死路 return false; // false表示不能往这个点走 } }else{ // 如果当前这个点已经走过,这个点可能是1,2,3 /* 如果这个点是1,表示是墙,不能走; 是3,表示走过,走不通; 是2,表示是已经走过的通路了,不要再重复走了 所以统一返回false */ return false; // false表示不能往这个点走 } } }
迷宫问题优化:存在最短路径
思路:改变策略,小球走过的路径和程序员设置的策略有关,将所有的策略都走一遍,将置成2的点放入一个ArrayList中,比较每个策略得出的结果的长度,最短的即最优,但时间复杂度过高,不推荐,后续学习深度遍历和广度遍历后有更优解法。
2.八皇后问题
八皇后问题也是一个回溯算法的典型案例。该问题描述为:
在8*8格的国际象棋上摆放八皇后,使其不能互相攻击,即任意两个皇后不能处于同一行、同一列或同一斜线上,问有多少种摆法。
思路:
1)第一个皇后放在第一行第一列。
2)第二个皇后放在第二行第一列,,然后判断是否ok,如果不ok,继续放在第二列、第三列、...,依次把所有列都放完,找到一个合适的。
3)继续第三个皇后,还是第一列、第二列、...直到第8个皇后也能放在一个不冲突的位置,算是找到了一个正确解。
4)当得到了一个正确解,在栈回退到上一个栈时,就会开始回溯,即将第一个皇后,放到第一列的所有正确解,全部得到。
5)然后回头继续第一个皇后放第二列,后面继续循环执行1,2,3,4的步骤。
说明:理论上应该创建一个二维数组来表示棋盘,但是实际上可以通过算法,用一个一维数组即可解决问题。如arr[8] = {0,4,7,5,2,6,1,3},行和下标对应,表示第几+1个皇后
因为是要找解法,所以回溯过程中不止是要判断是否被攻击,即使找到了通路,也要从栈顶元素开始继续遍历下一个点,查看是否有新的通路,比如说1234能走通,1235也能走通。
代码实现:
/* 思路:第一个皇后,放在第一行第一列;第二个皇后放在第二行第一列,判断是否冲突,如果冲突,后移; 不冲突,继续放第三个皇后,直到放完8个皇后 那么就在不冲突时:递归回溯放桓侯 同时,因为冲突的时候,需要后移,所以需要一个for循环 设置一个一维数组放置皇后放置位置的列数,下标就是列数,故不用二维数组 */ public class DemoQueue { static int max = 8; // 八皇后 static int[] arr = new int[max]; // 放置皇后,成员变量,共享列表 static int count = 0; // 有多少种解法 public static void main(String[] args) { // 测试 putQueue(0); // 从第0个皇后放起 System.out.println("一共有" + count + "种结果"); } // 放置当前皇后(递归回溯+for循环) private static void putQueue(int n){ // 回溯+循环停止条件 if(n == max){ // 8个都放好了 count++; print(); return; } // 循环,方便回溯的时候,下移 for (int i = 0; i < max; i++) { // 每个皇后都从该行第一列放起 arr[n] = i; // 然后判断位置是否冲突,冲突,下移(即继续循环),不冲突,递归下一个 if(judge(n)){ // 不冲突 putQueue(n + 1); } // 冲突,继续遍历,故啥也不写 } } // 判断当前皇后是否与前面的皇后位置冲突 private static boolean judge(int n){ // n表示第几个皇后(从0开始数) /*行冲突不用判断,放置方法决定了不会行冲突 列冲突:arr[n] == arr[i] 斜线冲突:| n - i | == | arr[n] - arr[i] | */ for (int i = 0; i < n; i++) { // 当前皇后和前面已经放好了的皇后进行比较 if(arr[n] == arr[i] || Math.abs(n - i) == Math.abs(arr[n] - arr[i])){ // 冲突 return false; // 冲突 } } return true; // 不冲突 } // 输出arr结果 private static void print(){ for(int i : arr){ System.out.print(i + " "); } System.out.println(); // 输出一种结果之后,就换行 } }
结果:
0 4 7 5 2 6 1 3
0 5 7 2 6 3 1 4
0 6 3 5 7 1 4 2
0 6 4 7 1 3 5 2
1 3 5 7 2 0 6 4
1 4 6 0 2 7 5 3
1 4 6 3 0 7 5 2
1 5 0 6 3 7 2 4
1 5 7 2 0 3 6 4
1 6 2 5 7 4 0 3
1 6 4 7 0 3 5 2
1 7 5 0 2 4 6 3
2 0 6 4 7 1 3 5
2 4 1 7 0 6 3 5
2 4 1 7 5 3 6 0
2 4 6 0 3 1 7 5
2 4 7 3 0 6 1 5
2 5 1 4 7 0 6 3
2 5 1 6 0 3 7 4
2 5 1 6 4 0 7 3
2 5 3 0 7 4 6 1
2 5 3 1 7 4 6 0
2 5 7 0 3 6 4 1
2 5 7 0 4 6 1 3
2 5 7 1 3 0 6 4
2 6 1 7 4 0 3 5
2 6 1 7 5 3 0 4
2 7 3 6 0 5 1 4
3 0 4 7 1 6 2 5
3 0 4 7 5 2 6 1
3 1 4 7 5 0 2 6
3 1 6 2 5 7 0 4
3 1 6 2 5 7 4 0
3 1 6 4 0 7 5 2
3 1 7 4 6 0 2 5
3 1 7 5 0 2 4 6
3 5 0 4 1 7 2 6
3 5 7 1 6 0 2 4
3 5 7 2 0 6 4 1
3 6 0 7 4 1 5 2
3 6 2 7 1 4 0 5
3 6 4 1 5 0 2 7
3 6 4 2 0 5 7 1
3 7 0 2 5 1 6 4
3 7 0 4 6 1 5 2
3 7 4 2 0 6 1 5
4 0 3 5 7 1 6 2
4 0 7 3 1 6 2 5
4 0 7 5 2 6 1 3
4 1 3 5 7 2 0 6
4 1 3 6 2 7 5 0
4 1 5 0 6 3 7 2
4 1 7 0 3 6 2 5
4 2 0 5 7 1 3 6
4 2 0 6 1 7 5 3
4 2 7 3 6 0 5 1
4 6 0 2 7 5 3 1
4 6 0 3 1 7 5 2
4 6 1 3 7 0 2 5
4 6 1 5 2 0 3 7
4 6 1 5 2 0 7 3
4 6 3 0 2 7 5 1
4 7 3 0 2 5 1 6
4 7 3 0 6 1 5 2
5 0 4 1 7 2 6 3
5 1 6 0 2 4 7 3
5 1 6 0 3 7 4 2
5 2 0 6 4 7 1 3
5 2 0 7 3 1 6 4
5 2 0 7 4 1 3 6
5 2 4 6 0 3 1 7
5 2 4 7 0 3 1 6
5 2 6 1 3 7 0 4
5 2 6 1 7 4 0 3
5 2 6 3 0 7 1 4
5 3 0 4 7 1 6 2
5 3 1 7 4 6 0 2
5 3 6 0 2 4 1 7
5 3 6 0 7 1 4 2
5 7 1 3 0 6 4 2
6 0 2 7 5 3 1 4
6 1 3 0 7 4 2 5
6 1 5 2 0 3 7 4
6 2 0 5 7 4 1 3
6 2 7 1 4 0 5 3
6 3 1 4 7 0 2 5
6 3 1 7 5 0 2 4
6 4 2 0 5 7 1 3
7 1 3 0 6 4 2 5
7 1 4 2 0 6 3 5
7 2 0 5 1 4 6 3
7 3 0 2 5 1 6 4
一共有92种结果