一、递归
1.1 概述
- 程序调用自身的编程技巧称为递归(
recursion
)。 - 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
- 递归的能力在于用有限的语句来定义对象的无限集合。一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。
1.2 应用场景
- 各种数学问题如: 八皇后问题 、汉诺塔、阶乘问题、迷宫问题、球和篮子的问题(google 编程大赛)。
- 各种算法中也会使用到递归,比如快排、归并排序、二分查找、分治算法等。
- 数据的结构形式是按递归定义的,如二叉树、广义表等,由于结构本身固有的递归特性,则它们的操作可递归地描述。
1.3 重要规则
-
执行一个方法时,就创建一个新的受保护的独立空间(栈空间)。
-
方法的局部变量是独立的,不会相互影响, 比如 n 变量。
-
如果方法中使用的是引用类型变量(比如数组),就会共享该引用类型的数据。
-
递归必须向退出递归的条件逼近,否则就是无限递归,会出现
StackOverflowError
。 -
当一个方法执行完毕,或者遇到
return
,就会返回。遵守谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕。
1.4 注意事项
- 递归算法解题相对常用的算法如普通循环等,运行效率较低。因此,应该尽量避免使用递归,除非没有更好的算法或者某种特定情况,递归更为适合的时候。
- 在递归调用的过程当中系统为每一层的返回点、局部量等开辟了栈来存储。
- 递归次数过多容易造成栈溢出等。
二、递归调用机制
- 代码示例:
public class RecursionTests {
public static void main(String[] args) {
test(4);
}
public static void test(int n) {
if (n > 2) {
test(n - 1);
}
System.out.printf("n=%d\n", n);
// n=2
// n=3
// n=4
}
}
- 示例说明:
- 当程序执行到一个方法时,就会开辟一个独立的空间(栈)。
- 每个空间的数据(局部变量),都是独立的。
- 示意图:
三、迷宫问题
需求:使用递归的方式,让小球到达目标位置。
- 示意图:
- 代码示例:
public class Maze {
/**
* 大写字母"X", 表示围墙或者障碍。
*/
private static final String OBSTACLE = "X";
/**
* 大写字母"O", 表示没有被走过。
*/
private static final String NO_WALK = "O";
/**
* 用字符"-", 标记走过的路径。
*/
private static final String PASS = "-";
/**
* 用字符"#", 表示寻路失败。
*/
private static final String FAIL = "#";
public static void main(String[] args) {
// 创建迷宫。
String[][] maze = new String[8][7];
int row = maze.length;
int col = maze[0].length;
// 设置围墙。
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
maze[i][j] = NO_WALK;
// 顶部 底部 最左列 最右列。
maze[0][j] = OBSTACLE;
maze[row - 1][j] = OBSTACLE;
maze[i][0] = OBSTACLE;
maze[i][col - 1] = OBSTACLE;
}
}
// 设置障碍。
maze[3][1] = OBSTACLE;
maze[3][2] = OBSTACLE;
System.out.println("当前创建好的迷宫为---: ");
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
System.out.print(maze[i][j] + " ");
}
System.out.println();
}
// 当前创建好的迷宫为---:
// X X X X X X X
// X O O O O O X
// X O O O O O X
// X X X O O O X
// X O O O O O X
// X O O O O O X
// X O O O O O X
// X X X X X X X
// 开始递归寻找路径。
setWay(maze, 1, 1);
System.out.println();
System.out.println("路径寻找后的迷宫为---: ");
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
System.out.print(maze[i][j] + " ");
}
System.out.println();
}
// 路径寻找后的迷宫为---:
// X X X X X X X
// X - O O O O X
// X - - - O O X
// X X X - O O X
// X O O - O O X
// X O O - O O X
// X O O - - - X
// X X X X X X X
}
/**
* 设置路径。
*
* @param maze 迷宫数组
* @param row 行
* @param col 列
* @return boolean
*/
public static boolean setWay(String[][] maze, int row, int col) {
// 被寻找的坐标。
if (PASS.equals(maze[6][5])) {
return true;
} else {
// 假设当前坐标没有走过。
if (NO_WALK.equals(maze[row][col])) {
maze[row][col] = PASS;
// 策略:下右上左的方式,递归寻找路径。
if (setWay(maze, row + 1, col)) {
return true;
} else if (setWay(maze, row, col + 1)) {
return true;
} else if (setWay(maze, row - 1, col)) {
return true;
} else if (setWay(maze, row, col - 1)) {
return true;
} else {
// 否则,就没有可走的路径(死路)。
maze[row][col] = FAIL;
return false;
}
} else {
// 当前坐标不是可移动的坐标。
return false;
}
}
}
}
四、八皇后问题
4.1 介绍
八皇后问题(英文:Eight queens),是由国际象棋棋手马克斯·贝瑟尔于1848年提出的问题,是回溯算法的典型案例。
- 问题表述为:在8×8格的国际象棋上摆放8个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。高斯认为有76种方案。
- 1854年在柏林的象棋杂志上不同的作者发表了40种不同的解,后来有人用图论的方法解出92种结果。如果经过±90度、±180度旋转,和对角线对称变换的摆法看成一类,共有42类。计算机发明后,有多种计算机语言可以编程解决此问题。
- 示意图:
4.2 思路分析
- 第一个皇后先放第一行第一列。
- 第二个皇后放在第二行第一列、然后判断是否 OK, 如果不 OK,继续放在第二列、第三列、依次把所有列都放完,找到一个合适。
- 继续第三个皇后,还是第一列、第二列……直到第 8 个皇后也能放在一个不冲突的位置,算是找到了一个正确解。
- 当得到一个正确解时,在栈回退到上一个栈时,就会开始回溯,即将第一个皇后,放到第一列的所有正确解,全部得到。
- 然后回头继续第一个皇后放第二列,后面继续循环执行 1,2,3,4 的步骤(即开始回溯)。
- 备注:理论上应该创建一个二维数组来表示棋盘,但是实际上可以通过算法,用一个一维数组即可解决问题.。举个例子 arr[8] ={0 , 4, 7, 5, 2, 6, 1, 3} ,即下标索引表示皇后所在行(第几个皇后),值表示皇后所在列。
4.3 代码示例
public class EightQueens {
private static final int MAX_SIZE = 8;
/**
* 用于存放八皇后解法的一维数组。
* 下标表示皇后所在行(第几个皇后),值表示皇后所在列。
*/
static int[] array = new int[MAX_SIZE];
/**
* 用于统计一共多少种解法。
*/
static int counter1 = 0;
/**
* 用于统计一共执行了多少次判断处理。
*/
static int counter2 = 0;
public static void main(String[] args) {
check(0);
// 04752613
// 05726314
// 06357142
// 06471352
// 13572064
// 14602753
// 14630752
// 15063724
// ... 此处省略其它结果
System.out.printf("共有 %d 种解法。", counter1);
System.out.printf("共执行判断 %d 次。", counter2);
// 共有 92 种解法。共执行判断 15720 次。
}
/**
* 检查并递归判断。
*
* @param queen 第几个皇后
*/
public static void check(int queen) {
// 说明八个皇后解法找完了。
if (queen == MAX_SIZE) {
print();
return;
}
for (int i = 0; i < MAX_SIZE; i++) {
array[queen] = i;
if (judge(queen)) {
// check()方法种包含judge(),此处是递归判断。
check(queen + 1);
}
}
}
/**
* 进行游戏规则判断。
*
* @param queen 第几个皇后
* @return boolean
*/
public static boolean judge(int queen) {
counter2++;
for (int i = 0; i < queen; i++) {
// 两个皇后是否在相同列,或者斜线位置。
// 因为每次皇后都在递增,所以没必要判断是否在同一行。
boolean isSameColum = array[i] == array[queen];
boolean isDiagonal = Math.abs(i - queen) == Math.abs(array[i] - array[queen]);
if (isSameColum || isDiagonal) {
return false;
}
}
return true;
}
/**
* 打印当前解法。
*/
public static void print() {
counter1++;
for (int e : array) {
System.out.print(e);
}
System.out.println();
}
}
五、结束语
“-------怕什么真理无穷,进一寸有一寸的欢喜。”
微信公众号搜索:饺子泡牛奶。