1 递归的特点
在一个子程序(过程或函数)的定义中又直接或间接地调用该子程序本身,称为递归。要使用递归算法,需满足一下几个条件:
(1) 需解决的问题有一个目标,且目标又可以分解为更小的子目标;
(2) 子目标的与原目标的实现方式完全相同;
(3) 存在最小子目标,即原目标经过有限次分解之后,子目标为一个已知常量(不可再细分);
(4) 由最小子目标的值可递推出最终目标;
凡是满足上述条件的问题,均可用递归的方法解决问题。
2 跟踪递归过程
参考如下的代码,分析递归过程。
函数int_to_ascii实现将unsigned 的int值转化为相应的字符序列。设将123传给函数int_to_ascii,第一次调用函数,将value的值压入堆栈,此时value=123,声明quotient后,quotient也将压入堆栈,但其值不确定,直到执行quotient=value/10后,quotient=12;因quotient不等于0,将进行下一次调用,此时并未执行到putchar这个函数;
第二次调用int_to_ascii,将重复第一次调用的过程,只是此时压入堆栈的value=12,随后声明quotient并将其压入堆栈,执行quotient=value/10后,quotient=1;因quotient不等于0,将进行下一次调用,此时并未执行到putchar这个函数;
第三次调用int_to_ascii,将重复上一次调用的过程,value=1,执行quotient=value/10后,quotient=0;因quotient等于0,将不再继续函数的自身调用,接下来执行putchar函数,因此时value=1,putchar函数将输出字符‘1’,随后函数返回,它的变量quotient和value(value=1和quotient=0)从堆栈中销毁,返回至函数的调用点(第二次函数调用if语句)之后的语句继续运行;于是接着执行第二次函数调用的putchar函数,此时在堆栈的函数变量vaule=12,所以putchar语句将输出字符‘2’,随后函数返回,它的变量(quotient和value)从堆栈销毁,继续执行调用点(第一次函数调用if语句)之后的语句,即第一次函数调用的putchar函数,因此时堆栈中的函数变量value=123,故输出字符‘1’,随后返回,它的变量从堆栈中销毁,第一次函数调用结束,整个递归过程到此结束,恢复到递归调用前的状态。
3 递归与迭代
用递归算法编写的程序结构清晰,具有很好的可读性,但其效率并高,有时甚至无法与非递归形式相提并论。菲波那契函数fibonacci就是一个例子,用迭代形式实现的菲波那契函数fibonacci函数描述如下:
当n>2时,每个递归将触发另外的两个递归调用,这样,冗余计算增长的很快。当递归计算fibonacci(10)时,fibonacci(3)被计算21次,而递归计算fibonacci(30)时,fibonacci(3)被计算317811次,这317811次计算完全是一样的,除一次之外,其余的纯属浪费,这个额外开销相当恐怖。
考虑非递归形式的fibonacci函数的实现,代码如下:
用这个非递归方法计算fibonacci(30),只需执行28次迭代即可,远小于用递归方法计算的次数,因此在用递归算法时,应考虑它的可读性是否抵得上它的代价,而且它的代价往往大的惊人,此时,应该考虑用迭代的方法来代替递归。那么什么情况下可以用迭代来代替递归呢?答案是当函数为尾部递归时,可用迭代代替递归。尾部递归是这样的一种递归:递归调用是函数执行的最后一项任务。举例来说,考虑求n的阶乘,用递归实现如下:
可用迭代的方法实现,代码如下:
尾部递归可以很方便的转化成一个简单的循环,完成相同的任务,而且省去了函数调用的系统开销,效率更高。