暴力求解法--递归调用

递归法:1.我们利用递归法往往是为了将问题简化,简化大致有以下两种。a.我们先做一部分工作,将剩下的给别人做,给别人做的问题和原问题一定是规模变小,形式相同的问题                                  b.我们在迈出第一步可以发现,原问题可以直接分解成多个规模更小,形式相同的子问题,这时就不用我们自己做

             2.什么时候用递归解决问题,a.用递归解决多重循环;b.原问题可以化简成规模更小,形式相同的子问题时;c.原问题是用递归来定义的。

             3.解决递归问题的步骤,我们在解决递归问题时应该先考虑怎么将问题进行简化;然后确定参数(参数可以描述出整个问题),我们可以在走第一步时通过走第一步用到什么参数来确定应有的参数,还有交给别人的工作中,用什么参数来描述这个工作作完了来确定应有的参数;确定返回条件,有几个if语句就代表需要满足几个条件,所以我们在一个问题中即使应该满足多个条件也不用担心,确定出所有的可能性,然后在返回条件中进行判断即可;

                 4递归的注意事项,a.有时后返回条件的先后会影响到运行结果;b.有的问题应该将之前做过的事清空,因为有的时候我们在回到上一层时在进行运算时不能受到之前运算的影响

               5.递归的形式  a.直线返回,这时是只有单个递归语句;b.多个递归语句时,这个时候只看一个递归语句仍是直线返回的,但是这个时候返回可以是方法里的所有语句都运行完了返回,也可以是到了返回条件返回;c.for循环里面加入递归语句,这时可以看成是循环往返

                  6.递归很多时候是自己完成一部分,或者一点不完成就将任务交给其他人,但是别人怎么完成这个问题我们不需要考虑太多,只要确定好正确的返回条件和返回值等就能得到想要的值

 

 

 

 

适宜于递归算法求解的问题的充分必要条件是:(1)问题具有某种可借用的类同自身的子问题描述的性质

(2)某一有限步的子问题(也称作本原问题)有直接的解存在,比如求阶乘时n==0的情况有直接解1的存在

	求n的阶乘    public int jisuan(int n) {
		if(n==0) {
			return 1;
		}
		else {
			return n*jisuan(n-1);
		}
		
	 // int sum=n*jisuan(n-1);
	 // return sum;  没有考虑到递归的回归条件
	}

观察问题时应该先观察是不是用递归形式定义的问题;将问题分解成更小的子问题进行求解,如果满足的话就利用递归来解决问题,思考问题的一开始可以先走两步,或者是考虑一般情况

 

中序遍历二叉树 private void inOrder(BiTreeNode t, Visit vs){  //中序   if(t != null){    inOrder(t.getLeft(),vs);    vs.print(t.data);    inOrder(t.getRight(),vs);  } }

 

求24问题

给出4个小于10的数,这四个数可以进行+-*/运算以及加上()形成一个表达式,问是不是存在一个结果使结果得到24

 

思考:4个数可以用数组进行存放;方法的变量有数组和未确定的数的个数(因为每次递归调用方法都要用到存放数的数组,所以数组在每次调用时都应该去掉已经确定的数);返回条件是未确定数的个数为1;这个问题可以想成先进行任意两个数的运算,将结果和其他的数一起形成一个新的数组

收获:这个问题跟爬楼梯与放苹果虽然都是将问题变成更小的问题但是有些不同,后两个是将问题直接分解成几个更小的问题,而本问题是先解决一部分然后在将剩下的变成一个整体进行方法的调用

           我们中间需要一个新的数组来存放两个数运算的结果和剩下的数,所以当数组就剩一个数那个数就是最后的结果 

 

 

  public boolean jisuan(double a[],int n) {
     if(n==1) {
      if(a[0]-24==0) {
       return true;
      }
      else return false;
     }
     double b[]=new double[5];
    for(int i=0;i<n-1;i++) {
     for(int j=i+1;j<n;j++) {
   int m=0;   
     for(int k=0;k<n;k++) {
      if(k!=i&&k!=j) {
       b[m++]=a[k];
      }
     }
     
   b[m]=a[i]+a[j];  
   if(jisuan(b,m+1)){
    return true;
   }
   b[m]=a[i]-a[j];
   if(jisuan(b,m+1)) {
    return true;
   }
   b[m]=a[j]-a[i];
   if(jisuan(b,m+1)) {
    return true;
   }
   b[m]=a[i]*a[j];
   if(jisuan(b,m+1)) {
    return true;
   }
   if(a[j]!=0) {
    b[m]=a[i]/a[j];
    if(jisuan(b,m+1)) {
     return true;
    }
   }
   if(a[i]!=0) {
    b[m]=a[j]/a[i];
    if(jisuan(b,m+1)) {
     return true;
    }
   }
     }
    }
 return false;
  }
 

 

 

n皇后问题

代码如下:

public void jisuan(int n,int a[],int cur){
       if(cur==n+1) {
    	   for(int i=1;i<a.length;i++) {
    	    	System.out.print(a[i]+" ");
    	    }
    	    System.out.println();
    	   return;
       }
       
       
		for(int i=1;i<=n;i++) {
			 int j;
			for(j=1;j<cur;j++) {
				if(i==a[j]||j-a[j]==cur-i||j+a[j]==cur+i) {
			     break;
			}
		}
			if(j==cur) {
			a[cur]=i;
			jisuan(n,a,cur+1);
			}
		}
}

 

//注意

int j;
   for( j=0;j<cur;j++) {
    if(a[j]==i||j-a[j]==cur-i||j+a[j]==cur+i) {
     break;
    }
     }
  if(j==cur) {} 此时j实在for循环外定义,这个时候可以利用for循环的j的值来判断循环是不是进行到了最后

收获:我们在for循环里使用到某个变量并且在for循环外也要用到这个变量那么定义变量时可以在for循环外定义

          我们分析问题时可以先走两步进行分析,也可以考虑一般情况进行分析

          重复调用方法时我们jisuan(n,a,cur++)不可以,得用jisuan(n,a,cur+1)

     分析问题时我们应该确定解决方法所用到的思想(是否是递归形式定义的问题,是否此问题可以分成更小的问题进行解决),方法需要的参数,方法调用返回时的条件

   我们不能有思维惯性,一定要把语句放在for循环或者if语句里,这里if语句是起到判断作用,但是和原来的判断不一样,它不是判断一行而是判断一直到cur的所有行是否符合条件,所以我们利用这种形式来确定是不是所有行都符合条件

 

 

注意:1.解决递归问题时应该注意递归的回归条件,可以使用return来让递归开使回归

          2.以上语句是实现求n!的代码,这个时候就利用了if语句和return语句设置了回归条件,在n变成0的时候回归,在二叉树的中序遍历的情况下是设置了节点不能为空的回归条件,如果不确定回归条件,有时会出现问题(比如n的阶乘中不考虑何时回归会让参数值出现错误)

           3.递归和普通方法调用一样是通过栈实现的

           4.递归可以用作  替代多重循环;解决本来就是用递归形式定义的问题;将问题分解成更小的子问题进行求解

           5.我们应该通过分析题目要求来确定方法需要的参数

 

 

爬楼梯:爬楼梯时我们可以一次爬一层或者爬两层,一共有n层,问一共有多少爬法

分析问题:参数是楼梯层数,调用方法的返回条件是楼梯剩下最后两层或一层,n个楼梯的爬法可以变成n-1个楼梯的爬法和n-2个楼梯的爬法。(这个问题可以分解成小的问题之和  有的问题可以解决一步后执行更小的问题)

代码如下:

public class test {
	public int jisuan(int n){
       if(n==1) return 1;
       if(n==2) return 2;
       
       return jisuan(n-1)+jisuan(n-2);
}

 

思考:有的问题需要我们先走一步在将工作让别人做,如n皇后问题等,还有将工作直接分开让别人做也能达到简化问题的目的

问题变形:如果爬楼梯的第一步是左脚,然后左右交替,要求最后一步应该是右脚,即走了偶数步,问此时一共有多少爬法。代码如下:

static long g(int n)
	{
		if(n==0) return 0;
		if(n==1) return 1;
		//if(n==2) return 1;
		
		return f(n-1) + f(n-2);
	}
	
	// 偶数步
	static long f(int n)
	{
		if(n==0) return 1;
		if(n==1) return 0;
		//if(n==2) return 1;
		
		return g(n-1) + g(n-2);
	}



或者是
	//如果爬楼梯的第一步是左脚,然后左右交替,要求最后一步应该是右脚,即走了偶数步,问此时一共有多少爬法。
	public int ceshi(int n,int flag){
		if(n==0&&flag%2==0){
			return 1;
		}
		if(n<0){
			return 0;
		}
		
		return ceshi(n-1,flag+1)+ceshi(n-2,flag+1);
	}

思考:递归调用不光是自己调用自己,也可以是互调,比如这个题走奇数步时我们调用一个方法,走偶数步时我们调用另一个方法。

 

放苹果问题:将m个苹果放到n个盘子里问一共有多少放法

分析:方法的参数是m与n;方法的返回条件一开始考虑错了,可以先考虑除了返回条件的代码如何书写,因为返回条件大部分是方法的参数满足某个值return个值或者只return,所以先看方法的参数在每次调用时会进行怎样的计算,比如走楼梯问题中,n是会n-1,结合实际问题我们考虑n==2和n==1的时候,此时m会经历m-n,n会经历n-1,结合实际问题确定我们的返回条件可以是n==0和m==0,并确定返回值。

收获:在确定盘子的放法时我们通过一些分析进行分类有苹果大于盘子的情况和苹果小于盘子的情况(苹果数小于盘子数又分为有空盘子和没有空盘子两种情况),不同的情况有不同的算法

public int jisuan(int m,int n) {
		//m个苹果放在n个盘子里一共有多少放法
		if(m==0) return 1;
		if(n==0) return 0;
		
		if(m<n) {
			return jisuan(m,m);
		}
		return jisuan(m,n-1)+jisuan(m-n,n);   //这是m>=n的情况,如果也放在if语句里面,会报错,因为if语句外也需要返回值,所以我们写在了外面
	}

思考:这个题将m和n的大小进行比较,将m<n当作一种情况,并且具体解决这个情况是在m》=n中,又进一步的分解情况

 

 

放盘子问题和爬楼梯和算24问题都是将问题转化成更小的问题这种形式

逆波兰表达式问题是用递归解决形式是递归的问题

 

 

例题:生成1-n的排列:得到用户输入的数n,输出1到n的全排列

分析:生成1-n的排列的问题,就是n个数的排列,我们可以在确定一个数后,n个数排列问题就变成n-1个数的排列

所需的变量,我们因为需要输出全排列,所以我们需要一个数组来存放全排列,返回条件是数组中放了n个数

代码如下:

 public void jisuan(int n,int a[],int cur) {
		if(cur==n) {
			for(int i=0;i<a.length;i++) {
				System.out.print(a[i]+" ");
			}
			System.out.println();
			//return;
		}
		else for(int i=1;i<=n;i++) {
		  int ok=1;
		  for(int j=0;j<cur;j++) {
			  if(a[j]==i) ok=0;
		  }
		  if(ok==1) {
		  a[cur]=i;
		  jisuan(n,a,cur+1);
	  }}
	 }

 

思考:此问题跟n皇后类似,n皇后是放好一个皇后后,在解决n-1皇后问题(皇后的放置位置应判定是否正确)

          此问题是先在数组里放好一个数后,在进行n-1个数的排列(放的数的值不能重复,也应该进行判定)

          这个题如果不把jisuan(n,a,cur+1)放在if语句里就会出错

 

n个数的排列的问题可以在放置1个数后,变成n-1个数的排列的问题

 

 

子集生成问题:得到用户输入的数n,然后求出1-n的子集,

代码如下:

	 public void jisuan(int n,int a[],int cur) {
		 for(int i=0;i<cur;i++) {
			 System.out.print(a[i]+" ");
		 }
		 System.out.println();
		 for(int i=1;i<=n;i++) {
			 boolean ok=true;
			 for(int j=0;j<cur;j++) {
				 if(a[j]==i) ok=false;
			 }
			 if(ok) {
			 a[cur]=i;
			 jisuan(n,a,cur+1);
		 }}
	 }

此时代码求出的子集会有重复,比如(1,2)和(2,1)

 

 

解决问题后的代码如下:

public void jisuan(int n,int a[],int cur) {
		 for(int i=0;i<cur;i++) {
			 System.out.print(a[i]+" ");
		 }
		 System.out.println();
		 int s;
		 if(cur==0) {
			 s=1;
		 }
		 else s=a[cur-1]+1;
		 for(int i=s;i<=n;i++) {
			 a[cur]=i;
			 jisuan(n,a,cur+1);
		 }
	 }

我们通过int s;
if(cur==0) {
s=1;
}
else s=a[cur-1]+1;

这些代码体现了定序的技巧,规定了数组中所有元素的编号从小到大排列,就不会出现(1,2)和(2,1)这样的情况了

 

素数环问题:输入正整数n,把整数1,2,……n,组成一个环,使得相邻两个整数之和均为素数。输出时从整数1开始逆时针排列。同一个环应恰好输出一次

代码如下:

 public void jisuan(int n,int a[],int cur) {
		 if(cur==n&&sushu(a[0],a[n-1])) {
			 for(int i=0;i<a.length;i++) {
				 System.out.print(a[i]+" ");
			 }
			 System.out.println();
		 }
		 
		for(int i=1;i<=n;i++) {
			 int ok=1;
			 for(int j=0;j<cur;j++) {
			 if(a[j]==i||!sushu(a[cur-1],i)) {
                ok=0;				 
			 }
			 }
			 if(ok==1) {
			 a[cur]=i;
			 jisuan(n,a,cur+1);
			 }
			}
	 }
	
    public boolean sushu(int a,int b) {
    	int num=a+b;
    	for(int i=2;i<num;i++) {
    		if(num%i==0) {
    			return false;
    		}
    	}
    	return true;
    }


回溯法是递归算法的一种特殊形式,回溯法的基本思想是:对一个包括有很多结点,每个结点有若干个搜索分支的问题,把原问题分解为对若干个子问题求解的算法。当搜索到某个结点、发现无法再继续搜索下去时,就让搜索过程回溯退到该结点的前一结点,继续搜索这个结点的其他尚未搜索过的分支;如果发现这个结点也无法再继续搜索下去时,就让搜索过程回溯(即退回)到这个结点的前一结点继续这样的搜索过程;这样的搜索过程一直进行到搜索到问题的解或搜索完了全部可搜索分支没有解存在为止。 

 

例题:公园票价为5角假设·每位游客只持有两种币值的货币,5角和1元。在假设持有5角的有m人,持有1元的有n人。由于特殊时期,开始的时候,售货员没有零钱可找。求这些m+n个游客以什么样的顺序购票则可以顺利完成购票过程。计算出这m+n个游客所有可能顺利完成购票的不同情况的组合数目。  注意:只关心5角和1元交替出现的次序的不同排列,持有同样币值的两名游客交换位置并不算做一种新的情况来计算。代码如下:

public int jisuan(int m,int n,int k) {
		 //m代表5毛的钱的个数,n代表1元的钱的个数,k代表售票员拥有5角的个数
		 if(m+k<n) return 0;
		 if(n==0||m==0) {
			 return 1;
		 }
		  if(k==0) {
			 return jisuan(m-1,n,k+1);
		 }
		  else return jisuan(m-1,n,k+1)+jisuan(m,n-1,k-1);
	 }

思考:我们利用递归来解决问题时,所用到的参数一定是能完整表示这个问题的,这个问题中售货员有没有5角钱也很重要,所以我们有了参数k;我们在考虑回归条件时不能想当然,应该考虑一下各个参数的极限情况,比如这个题一开始只考虑了n==0时回归,没有想到m==0时仍然可以回归;还有不满足情况时的回归条件我一开始考虑的不够全面

 

例题:汉诺塔问题

汉诺塔问题的描述是:设有3根标号为A,B,C的柱子,在A柱上放着n个盘子,每一个都比下面的略小一点,要求把A柱上的盘子全部移到C柱上,移动的规则是:

    (1)一次只能移动一个盘子;

    (2)移动过程中大盘子不能放在小盘子上面;

    (3)在移动过程中盘子可以放在A,B,C的任意一个柱子上。  

代码如下:

public static void towers(int n, char fromPeg, char toPeg, char auxPeg){
        if(n == 1){ 
	System.out.println("move disk 1 from peg " + fromPeg + " to peg " + toPeg);
	return;
        }
	
        towers(n -1, fromPeg, auxPeg, toPeg);
        System.out.println("move disk " + n + " from peg " + fromPeg + " to peg " + toPeg);
        towers(n - 1, auxPeg, toPeg, fromPeg);
}

 

思考:这个方法中用到的参数,后面三个字符都是没有实际意义的,但是对于描述完整的问题是不可缺少的,所以我们加上他们

          通过总结有了新的体会,递归就是自己完成一部分工作( System.out.println("move disk " + n + " from peg " + fromPeg + " to peg " + toPeg);
),然后在将剩下的工作推给别人来完成 (towers(n -1, fromPeg, auxPeg, toPeg)

有的问题中是自己先干一些然后把工作给了别人,比如求n的阶乘问题中,就是先拿出了n然后让别人算剩下的,这种问题的递归形式是会一直深入到最底,然后在逐个返回一直到最初的函数,有些类似与线的结构

有的问题中是先让别人干,最后在自己干,比如二叉树的遍历,这种问题的递归形式是先深入到最底然后返回到上一层,再尝试新的可能,倒数第二层的可能性没有之后再回到倒数第三层,寻找倒数第三层的所有可能。

有的问题是在for循环里面使用递归,具体递归形式跟前者相似,就是多了个for循环,要进行多次的循环

 

例题:匪警请拨110,即使手机欠费也可拨通!为了保障社会秩序,保护人民群众生命财产安全,警察叔叔需要与罪犯斗智斗勇,因而需要经常性地进行体力训练和智力训练!某批警察叔叔正在进行智力训练:1 2 3 4 5 6 7 8 9 = 110请看上边的算式,为了使等式成立,需要在数字间填入加号或者减号(可以不填,但不能填入其它符号)。之间没有填入符号的数字组合成一个数,例如:12+34+56+7-8+9 就是一种合格的填法;123+4+5+67-89 是另一个可能的答案。请你利用计算机的优势,帮助警察叔叔快速找到所有答案。每个答案占一行。形如:12+34+56+7-8+9123+4+5+67-89

代码如下:

static void f(int[] a, int k, String so, int goal){
			if(k==0){
				if(a[0] == goal){
					System.out.println(a[0]+so);
				}
				return;
			}
			
			f(a,k-1,"+"+a[k]+so, goal-a[k]);
			f(a,k-1,"-"+a[k]+so, goal+a[k]);
			int old = a[k-1];
			a[k-1] = Integer.parseInt("" + a[k-1] + a[k]);
			f(a,k-1,so,goal);
			a[k-1] = old;
		}

思考:此时我们的目标是将每一步都利用字符串记录下来,如果某一个可能的情况中得到了110,就输出字符串。

这个时候递归我们考虑从后往前递归,因为从后往前的话,感觉会简单一些。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值