在之前讲解函数内容时提到过函数自身调用自身叫做“递归”。那么为什么要用到递归?,下面我们对递归的内容和在程序中的使用进行介绍,来说明为什么会用到递归以及何时进行递归。
递归的定义及优点
递归的定义:递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
我们先来看一个简单的问题,之前在讲解循环的内容时,其中有道求解前100项和问题,我们用了一个for循环去解决,现在我们怎么用递归解决呢?我们知道,前一百项是从1开始即1+2+3+...+99+100,那么我们不妨这样想,前100项和是前99项和加上100得出的结果,而前99项和是前98项和加上99得出的结果,以此下去知道前1项和是从1开始的,那么这就是递归的终止条件,假如前n项和为F(n),那么F(100)=F(99)+100,而F(99)=F(98)+99....F(2)=F(1)+2,F(1)=1。不理解文字描述可参考下面的图:
因此用递归函数实现该功能的代码为:
class Demo01{
public static void main(String[] args){
int sum=f(100); //调用求和函数
System.out.println(sum);
}
//1+2+3+4+....+100
public static int f(int n){
if(n==1){ //如果求前1项和,则直接返回1
return 1;
}
return f(n-1)+n; //如果求和项大于1,就不断调用自身直到为1;
}
可能到这你还会觉得这和我们之前写的for循环的代码量差别不大,别急,我们再来看一个经典问题——斐波那契函数
斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家列昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,377,610,987,1597,2584,4181,6765,10946,17711,28657,46368........这个数列从第3项开始,每一项都等于前两项之和。如果按照for循环来写则是这样的:
class Demon02{
public static void main(String[] args){
fibo(30);
}
public static void fibo(int n){
int a=1; //前两项为1
int b=1;
int c=0; //存放第n项值
for(int i=1;i<=n;i++){
if(i==1||i==2){ //如果i为1或者2 ,则值为1
System.out.print(1+" ");
}else{ //否则从第三项开始的第i项
c=a+b; //等于前两个值的和
System.out.print(c+" "); //记录每项的值
a=b; //将第i-1赋给a
b=c; //第i项赋给b
}
}
}
}
如果用递归实现:
class Demon03{
public static void main(String[] args){
fibo_dg(30);
}
public static int fibo_dg(int n){
if(n==1||n==2){ //如果是第1项或是第2项
return 1; //直接将结果返回1
}
return fibo_dg(n-1)+fibo_dg(n-2); //如果n>=3,就调用n-1项,直到n-1为2,将后两项相加得出结果返回
}
}
对比 Demon02 和 Demon03 就会发现,用递归解决问题的代码会比用for循环解决的代码简单的多,只是代码看起来没有那么很好的理解,所以在用递归解决问题时,一定要将问题的逻辑和自己的思路理清。
递归的缺点
虽然递归是简化了代码,看起来也很简洁,但是“人无完人”,递归也存在弊端。我们之前在讲函数时,提到过函数的内存调用,而递归的前提就是先分装成一个函数,然后调用自身,因为主函数是无法自己调用自己的,那么就说明了一个问题,就是函数的运行空间——“栈内存”是无限大的吗?显然这不是的,我们先来看一个栗子:
class Demon04{
public static void main(String[] args){
show();
}
public static void show(){
System.out.println("show...");
show();
}
}
这里void会不断地调用自身,并且没有给它一个结束的条件,我们来看一下运行的结果
起初可能会不断的打印show()...,但是当达到极限的时候,就会报错,出现一个叫StackOverflowError的错误,这个错误叫栈溢出错误,原因是,程序在不断的加载新的show()函数,将栈区间占用满了,所以不能再加载新的函数进栈,是因为之前进栈的所有函数都没有处理完,还占用着栈内空间。所以在计算递归的层数越大,程序运行的越慢,最后可能因为加载不下而导致栈溢出错误。
汉诺塔问题
虽然递归存在弊端,但是我们也不能忽略它的好处。在汉诺塔问题中,诠释了递归的实用性。我们先来看一下问题的需求:有三根相邻的柱子,标号为A,B,C,A柱子上从下到上按金字塔状叠放着n个不同大小的圆盘,要把所有盘子一个一个移动到柱子C上,并且在每次移动中同一根柱子上都不能出现大盘子在小盘子上方这种情况,要求给出移动的步骤。
如果是人为自己思考的话,第一步先怎么移以及第二步再怎么移都是知道,如果要将其转化为程序语言,我们就要判断不同情况的不同处理办法。如果盘子数量少的话,我们可以用判断加循环将其解决,那随着盘子的数量增多呢,就要写一大堆的判断这是相当麻烦的,而且程序的可读性也会降低,所以我们要找到通用的办法去解决问题。
首先我们肯定是把上面n-1个盘子移动到柱子B上,然后把最大的一块放在C上,然后把B上的n-2个盘子移动到A上,把B上最大的那个盘子再放到C上,这样依次分解。假如我们以5个盘子为例,
这是从后往前考虑,想要将最大的盘子放在下面,就要将上面小的盘子都挪走,那么上面小的盘子又有一个最大的盘子,又再次考虑将第二大的挪走依次类推,直到递归到最小的那个盘子,因此我们的程序为:
import java.util.Scanner;
class Hano{
public static void main(String[] args){
Scanner scanner =new Scanner(System.in);
System.out.print("请输入盘子的个数:"); //首先接受要处理的盘子个数
int level=scanner.nextInt();
move("x","y","z",level); //x,y,z表示三个柱子,level表示盘子的数量
}
public static void move(String from,String mid,String to,int level){ //规定盘子要从X到Y
if(level==1){ //如果盘子数为1,将盘子从X移到Y
System.out.println(from+"->"+to);
}else{ //否则递归调用,每次移动借助的柱子编号是不一样的
move(from,to,mid,level-1); //移动前n-1项
System.out.println(from+"->"+to); //每移动一次就打印步骤
move(mid,from,to,level-1);
}
}
}
此问题比较难,建议将每一步都自己操作一番进行理解。