因为在树的内容中,要大量遇到递归的操作,所以着这里复习一下递归操作。
递归,就是自己调用自己。
首先,需要搞清楚函数是如何调用的。在执行被调函数之前,系统需要做3件事:
1.将实参,函数的返回地址等信息传递给被调函数保存。
2.为被掉函数的形参,局部变量分配空间
3.将控制转移到被调函数入口。
当从被调函数返回前,也需要做3件事:
1.保存函数的返回结果
2.释放被调函数存储空间
3.按照被调函数保存的返回地址将控制转移给调用函数。
其中主调函数和被调函数间的参数传递和控制转移是通过栈来实现的:计算机只能操作栈顶元素。当调用被掉函数时,将被调函数的数据空间压栈,则即将控制权转移给了被掉函数;当函数结束时,被调函数出栈,则控制权又返回给了主调函数。
从此可以看出,计算机程序调用别的程序和调用自己是没有区别的,同样都是做上述事情。
让我们从一个简单的递归问题开始吧,求1到100的和。
//求和
int sum(int n)
{
if(0 == n)
return 0;
else
return n+sum(n-1);
}
它的求解思路是这样的:虽然我不知道0+……+100的和是多少,但是我知道0+……+100的和肯定等于0+……+99的和再加上100;那么0+……+99的和又等于多少呢,我不知道,但是我知道他肯定等于0+……+98的和再加上99;依次这样类推,总会有:那么0自己的和是多少呢(这样说很坑爹),答案是0,所以能从0自己的和推出0+1的和,推出0+1+2的和,此次就能计算出0+……+100的和了。
与一般的迭代相比递归有这么几个特点:
1.递归更贴近于人的“逆向思维”,而迭代却是不动脑子的直接做。
2.从程序的复杂度上来说,递归比较短小,而迭代会比较长。
3.从执行效率来讲,递归效率低(因为函数调用要做许多事),而迭代效率较高。
4.当程序中有bug时,迭代的错误往往容易发现,而递归的错误找起来却很费力。
到底什么时候用递归,什么时候用迭代的呢?如果你想少用一点自己脑子,而让计算机多累一点,那么就用递归;如果你想让自己考虑清楚所有的步骤,计算机直接照着执行,那么就用迭代。
其实,并不是所有的问题都能转化成递归问题的,当使用递归分析问题时,必须保证递归以后,问题的本质没有发生变化,但是规模减小了,而且当规模小到一定程度时,我能求解他。
下面再看几个经典的递归程序的例子:
//求阶乘
int factorial(int n)
{
if (n == 1)
return 1;
else
return n*factorial(n-1);
}
//Fibonacci数列
long Fibonacci(int n)
{
if(n == 0)
return 0;
else if(n == 1)
return 1;
else
return Fibonacci(n-1)+Fibonacci(n-2);
}
//汉诺塔
void hanNuoTa(int n,char A,char B,char C)
{
if( 1 == n )
printf("move disk %d from %c to %c\n",n,A,C);
else
{
hanNuoTa(n-1,A,C,B);
printf("move disk %d from %c to %c\n",n,A,C);
hanNuoTa(n-1,B,A,C);
}
}
尤其是汉诺塔,非常值得玩味。但是由于说起来很啰唆,这里就不提了。
最后送一句名言给大家:To iterate is human,to recurse divine.—— L. Peter Deutsch。这句话非常值得玩味,到底是说迭代是人,递归是神,还是想说一般人你就迭代做吧,如果你是高手,那么再用递归,恐怕没人能说得清。