1、递归
简单的来说递归就是一个函数直接或间接地调用自身,是为直接或间接递归。一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。用递归需要注意以下两点:(1) 递归就是在过程或函数里调用自身。(2) 在使用递归策略时,必须有一个明确的递归结束条件,称为递归出口。
1 int FibonacciRecursive(int n)
2 {
3 if( n < 2)
4 return n;
5 return (FibonacciRecursive(n-1)+FibonacciRecursive(n-2));
6 }
递归写的代码非常容易懂,完全是根据函数的条件进行选择计算机步骤。例如现在要计算n=5时的值,递归调用过程如下图所示:
2、尾递归
如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。
尾递归就是从最后开始计算, 每递归一次就算出相应的结果, 也就是说, 函数调用出现在调用者函数的尾部, 因为是尾部, 所以根本没有必要去保存任何局部变量. 直接让被调用的函数返回时越过调用者, 返回到调用者的调用者去。尾递归就是把当前的运算结果(或路径)放在参数里传给下层函数,深层函数所面对的不是越来越简单的问题,而是越来越复杂的问题,因为参数里带有前面若干步的运算路径。
尾递归是极其重要的,不用尾递归,函数的堆栈耗用难以估量,需要保存很多中间函数的堆栈。比如f(n, sum) = f(n-1) + value(n) + sum; 会保存n个函数调用堆栈,而使用尾递归f(n, sum) = f(n-1, sum+value(n)); 这样则只保留后一个函数堆栈即可,之前的可优化删去。
尾递归函数的特点是在回归过程中不用做任何操作,当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活动记录而不是在栈中去创建一个新的。编译器可以做到这点,因为递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的运行效率会变得更高。
(注1:事实上,在C#中,即使您实现了尾递归,编译器(包括C#编译器及JIT)也不会进行优化,也就是说还是无法避免StackOverflowException。)
尾递归的过程和循环基本上是等价的,我们可以将尾递归的过程很方便到用循环来代替,把尾递归优化为“循环”方式。
采用尾递归实现Fibonacci函数,程序如下所示:
1 int FibonacciTailRecursive(int n,int ret1,int ret2)
2 {
3 if(n==0)
4 return ret1;
5 return FibonacciTailRecursive(n-1,ret2,ret1+ret2);
6 }
例如现在要计算n=5时的值,尾递归调用过程如下图所示:
从图可以看出,尾递归不需要向上返回了,但是需要引入而外的两个空间来保持当前的结果。