数据结构和算法第六章递归
数据结构和算法第六章递归
文章目录
前言
数据结构和算法第六章递归
一、递归与回溯?
一、递归与回溯概念的理解
1)程序调用自身的编程技巧称为递归( recursion):
递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为
一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
2)回溯思路:
回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。
回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,
这种走不通就退回再走的技术为回溯法。
回溯的思路基本如下:当前局面下,我们有若干种选择,所以我们对每一种选择进行尝试。如果发现某种选择违反了某些限定条件,此时 return;
如果尝试某种选择到了最后,发现该选择是正确解,那么就将其加入到解集中。
在这种思想下,我们需要清晰的找出三个要素:选择 (Options),限制 (Restraints),结束条件 (Termination)。
3)用一个比较通俗的说法来解释递归和回溯:
我们在路上走着,前面是一个多岔路口,因为我们并不知道应该走哪条路,所以我们需要尝试。尝试的过程就是一个函数。
我们选择了一个方向,后来发现又有一个多岔路口,这时候又需要进行一次选择。所以我们需要在上一次尝试结果的基础上,
再做一次尝试,即在函数内部再调用一次函数,这就是【递归】的过程。
这样重复了若干次之后,发现这次选择的这条路走不通,这时候我们知道我们上一个路口选错了,所以我们要回到上一个路口重新选择其他路,这就是【回溯】的思想。
二、递归需要遵守的重要规则:
1) 执行一个方法时,就创建一个新的受保护的独立空间(栈空间)
*
递归的底层用到了栈,当程序执行到一个方法时,就会开辟一个独立的空间(栈空间),它是独立的,好像把每个空间放到一个大的栈里面去,每次执行的时候,是把新的栈压在栈顶,当然执行也是从上面开始执行的,执行完了回到下一个栈,最后回到栈低,main栈的时候,程序结束,递归的底层、递归编译器的底层用到了栈的机制
*
2) 方法的局部变量是独立的,不会相互影响, 比如 n 变量
3) 如果方法中使用的是引用类型变量(比如数组),就会共享该引用类型的数据,(迷宫问题,让多个栈共享同一个数组,不管调用产生多少个栈,都是修改同一张
地图)
4) 递归必须向退出递归的条件逼近,否则就是无限递归,出现 StackOverflowError,死递归了:(比如打印问题中test(n-1))
不停的递归,出现栈溢出(StackOverflowError)
5) 当一个方法执行完毕,或者遇到 return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕
二、递归用于解决的问题
- 各种数学问题如: 8皇后问题 , 汉诺塔, 阶乘问题, 迷宫问题, 球和篮子的问题(google 编程大赛)
- 各种算法中也会使用到递归,比如快排,归并排序,二分查找,分治算法等.
- 将用栈解决的问题–>递归代码比较简洁
1.打印问题和阶乘问题回顾递归调用机制
递归的调用机制:通过打印问题,回顾递归调用机制
递归的调用规则:
1,当程序执行到一个方法时,就会开辟一个独立的空间(栈帧)
2,每个空间的数据(局部变量),是独立的(比如图解中:每个栈帧都有一个n变量,互不影响)
3,递归的一个方法执行完毕,或者遇到ruturn,就会返回到调用该方法的地方
图解:打印问题回顾递归调用机制.png
/*
打印问题
*/
public static void test(int n){
if(n>2){
test(n-1);
}
System.out.println("n="+n);
}
/*
阶乘问题
*/
public static int factorial(int n){
if(n==1){
return 1;
}else{
return factorial(n-1)*n;
}
}
2.递归–迷宫问题(小球找路问题)
public static void main(String[] args) {
//创建二维数组,模拟迷宫,使用1为墙,上下置为1,左右置为1,设置挡板为1
//创建一个8行7列的二维数组
int[][] map = new int[8][7];
//将上下置为1
for (int i = 0; i < 7; i++) {
//将第一行置为1,最后一行置为1,做墙
map[0][i] = 1;
map[7][i] = 1;
}
//将左右置为1
for (int i = 0; i < 8; i++) {
//将第一列和最后一列置为1
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();
}
//小球找路,小球的起始位置在(1,1)位置
setWay(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();
}
}
/**
* @param map 地图
* @param i 小球起始位置所在的行
* @param j 小球起始位置所在的列
* @return 如果找到通路,就返回true,否则返回false
* 通路即为小球能到map[6][5]位置,就说明通路找到
* 约定:当map[i][j]==0表示该点还没有走过
* ==1表示墙
* ==2表示通路可以走
* ==3表示该点已经走过,但是走不通
* 走迷宫时,需要确定一个策略(方法)下-->右-->上-->左
*/
//小球找路,将地图,与起始位置传过来
public static boolean setWay(int[][] map, int i, int j) {
//判断如果直接map[6][5]为2时,表示该路可走,通路已经找到,直接返回true
if (map[6][5] == 2) {
return true;
} else {
if (map[i][j] == 0) {//如果该点换没走过
//如果该点还没走过,就按照上面规定的策略即方法走
//先假定该点可以走通,将能走通的点标记为2,下次回溯就可以不用再走啦,要不然回溯太多容易出现栈溢出StackOverflow
map[i][j]=2;
if (setWay(map, i + 1, j)) {//先向下走
return true;
} else if (setWay(map, i, j + 1)) {//再向右走
return true;
} else if (setWay(map, i - 1, j)) {//再向上走
return true;
} else if (setWay(map, i, j - 1)) {//再向左走
return true;
}else{//说明该点走不通,是死路
map[i][j]=3;
return false;
}
} else {//该点已经走过,即就是当map[i][j]=1,2,3这三种情况表示都已经走过,不需要再走,直接返回false
return false;
}
}
}
3.递归–八皇后问题
八皇后问题:任意两个皇后都不能处于同一行、 同一列或同一斜线上,问有多少种摆法(92种)
八皇后问题算法思路分析:
- 第一个皇后先放第一行第一列
- 第二个皇后放在第二行第一列(编写一个方法,放第n个皇后)、然后判断是否 OK(查看我们放置第n个皇后,就去检测该皇后是否和前面已经摆放的皇后冲突), 如果
不 OK,继续放在第二列、第三列、依次把所有列都放完,找到一个合适 - 继续第三个皇后,还是第一列、第二列……直到第 8 个皇后也能放在一个不冲突的位置,算是找到了一个正确解
- 当得到一个正确解时,在栈回退到上一个栈时,就会开始回溯,即将第一个皇后,放到第一列的所有正确解,全部得到.
- 然后回头继续第一个皇后放第二列,后面继续循环执行 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 列}
//先定义一个max来表示有几个皇后
int max=8;
//定义一个计数变量来统计打印了多少次,即就是有多少种解法
static int count=0;
//定义一个计数变量judgeCount来统计一共判断了多少次,
static int judgeCount=0;
//创建一个一维数组,arr[i] = val , val 表示第 i+1 个皇后,放在第 i+1 行的第 val+1 列
//理论上应创建一个二维数组来表示棋盘,但其实可用一个一维数组来解决问题
int[] array=new int[max];
public static void main(String[] args) {
//创建对象调用方法
递归八皇后问题 queue8 =new 递归八皇后问题();
queue8.check(0);
System.out.println("8皇后的解法一共有:"+count+"种解法");
System.out.println("一共判断冲突的次数为%d:"+judgeCount+"次");
}
//编写一个方法来放第n个皇后
void check(int n){
//如果n为8,就表示8个皇后已经放好,就将其打印,并退出
if(n==8){
print();
count++;
return;
}
//依次放入皇后,并判断是否冲突
for(int i=0;i<max;i++){
//先将当前这个皇后n放在第一列
array[n]=i;
//判断第n个皇后放在第i列时是否冲突
if(judge(n)){//不冲突时
//接着放第n+1个皇后,即开始递归
check(n+1);
}
//如果冲突的话,就继续执行array[n]=i;将第n个皇后放在第i+1列,即本行后移的一个位置
}
}
//查看我们放置第n个皇后,就去检测该皇后是否和前面已经摆放的皇后冲突
private boolean judge(int n){
judgeCount++;
for(int i=0;i<n;i++){
//由于每次放第n个皇后其行都在加1,故不需要判断其是否在同一行
//arr[i]==arr[n]表示第n个皇后和第i个皇后在同一行
//|arr[n]-arr[i]|=(n-i)表示第n个皇后和第i个皇后在同一斜线上
if(Math.abs(array[n]-array[i])==Math.abs(n-i)||array[i]==array[n]){
return false;
}
}
return true;
}
//写一个方法可以将皇后的摆放的位置输出
private void print(){
for(int i=0;i<array.length;i++){
System.out.print(array[i]+" ");
}
System.out.println();
}