对于求 n!,你可能会问,为何要用递归?有何优势?答案并不复杂,利用递归可以使算法的逻辑变得非常简单。因为递归过程的每一步用的都是同一个算法,计算机只需要自顶向下不断重复即可。
具体到阶乘的计算,无非就是某个数字 n 的阶乘,变成这个数乘以 n-1 的阶乘。因此,递归的法则就两条:一是自顶而下(从目标直接出发),二是不断重复。
递归的另一个特点在于,它只关心自己下一层的细节,而并不关心更下层的细节。你可以理解为,递归的简单源自它只关注“当下”,把握“小趋势”,虽然每一步都简单,但一直追寻下去,也能获得自己独特的精彩。
下面我们就以计算阶乘为例,分别使用递推和递归方式实现,见例 1,读者可体会二者的区别。
【例 1 】利用递推和递归方式分别计算 n!(iterative-recursive.py)。
#用正向递推的方式计算阶乘
def iterative_fact(n):
fact = 1
for i in range(1, n + 1):
fact *= i
return fact
#用逆向递归的方式计算阶乘
def recursive_fact(n):
if n <= 1 :
return n
return n * recursive_fact(n - 1)
#调用递推方法计算
num = 5
result = iterative_fact(num)
print("递推方法:{} != {}".format(num, result))
#调用递归方法计算
result = recursive_fact(num)
print ("递归方法:{} != {}".format(num,result))
程序执行结果为:
递推方法:5 != 120
递归方法:5 != 120
第 02~06 行定义了一个递推计算阶乘的函数 iterative_fact( ),函数内部采用 for 循环的方式来计算结果。在 for 循环控制过程中使用了 range( ) 函数,由于 range( ) 的取值区间是左闭右开的,最后一个值取不到,所以在第 04 行执行了 n+1 操作。
第 09~12 行定义一个递归函数 recursive_fact( ),采用递归的方式计算结果。
第 17 行和第 20 行用到了 Python 的格式化输出。在 Python 中,一切皆对象。用双引号引起来的字符串“递归方法:{}!= {}”,实际上是一个 str 对象。既然是对象,它就会有相应的方法成员,format( ) 就是用于格式化输出的方法,因此可以通过“对象.方法名”的格式来调用合适的方法。
字符串中的花括号{ }表示输出占位符,第 1 个占位符{ }用于输出 format( ) 函数中第 1 个变量,第 2 个占位符{ }用于输出 format( ) 函数中第 2 个变量,以此类推。
递归函数的优点在于,定义简单,逻辑清晰。理论上,所有的递归函数都可以写成循环的方式,但正向递推(即循环)的逻辑不如逆向递归的逻辑清晰。
对于递推的实现,这里用到了前面章节中讲到的 for 循环语句,以 1 为基数不断循环相乘,最终得出阶乘的结果。而在递归实现的操作中,这里通过对方法本身压栈和弹栈的方式,将每一层的结果逐级返回,通过逐步累加求得结果。
recursive_fact(5)的计算过程如下。
===> recursive_fact(5)
===> 5 * recursive_fact(4)
===> 5 * (4 * recursive_fact(3))
===> 5 * (4 * (3 * recursive_fact(2))
===> 5 * (4 * (3 * (2 * recursive_fact(1))))
===> 5 * (4 * (3 * (2 * 1)))
===> 5 * (4 * (3*2))
===> 5 *(4*6)
===> 5 * 24
===> 120
需要注意的是,虽然递归有许多的优点,但缺点也很明显。那就是,使用递归方式需要函数做大量的压栈和弹栈操作,由于压栈和弹栈涉及函数执行上下文(context)的现场保存和现场恢复,所以程序的运行速度比不用递归实现要慢。
此外,大量的堆栈操作消耗的内存资源要比非递归调用多。而且,过深的递归调用还可能会导致堆栈溢出。如果操作不慎,还容易出现死循环。因此读者编写代码时需要多加注意,一定要设置递归操作的终止条件。