递归的定义:在数学和计算机科学中,递归是指在函数的定义中调用函数自身的方法。
递归举例:斐波那契数列。
>>> def fib(n):... '''This is Fibonacci by Recursion'''... if n == 0: # 函数结束的条件... return 0... elif n == 1:... return 1... else:... return fib(n-1) + fib(n-2) # 递归,定义函数的内部调用函数自身...>>> fib(10)55
从上面的计算过程可以看出,每个递归的过程都是向着最初的已知条件(结束条件)方向挺近一步,直到通过这个最底层的条件得到结果,然后再一层一层向上回馈(返回)计算结果。
将上面的例子优化:
>>> m = {0:0, 1:1} # n=1和n=0的值已知,直接定义出来,省去每次的多个判断>>> def fib(n):... if not n in m:... m[n] = fib(n-1) + fib(n-2)... return m[n]...>>> fib(10)55
递归的要素:
结束条件:满足条件时,函数调用结束,直接返回一个值,即有一个明确的递归结束条件,称为递归出口。
递归调用:包含一个或多个调用,这些调用旨在解决问题的一部分。
这里的关键是,通过一次次的分解问题,将问题分解为较小的部分,最终分解到结束条件。
递归的回溯与递推:
递推:像上边递归实现所拆解,递归每一次都是基于上一次进行下一次的执行,这叫递推;
回溯:则是在遇到终止条件,则从最后往回返,一级一级的把值返回,这叫回溯。
>>> lst =[1, 2, [3, [4, 5, 6, [7, 8, [9, 10, [11, 12, 13, [14, 15,[16,[17,]],19]]]]]]]>>> def search(lst):... for item in lst:... if type(item) is list:... search(item)... else: # 递归调用结束条件... print(item, end=' ') ...>>> search(lst)1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 19
深刻理解:每次递归调用函数时,都将为这个递归调用的函数创建一个新的命名空间。各自的命名空间是相互独立的,每个递归函数在各自的命名空间内生存并依次按顺序执行;当执行到结束条件时,从结束的位置开始向前一次次的反馈(返回)并继续执行每个命名空间内剩余的部分。关键在于每次递归时都会创建各自相互独立的命名空间。
两个经典案例:阶乘和幂
阶乘:1的阶乘为1;
对于大于1的数字n,其阶乘为n-1的阶乘再乘以n。
>>> def func(n):... if n == 1:... return 1... return n * func(n-1)...>>> func(5)120
幂:对于任何数字x,power(x, 0)都为1;
n>0时,power(x, n)为power(x, n-1)与x的乘积。
>>> def func(n, m):... if m == 0:... return 1... if m < 0:... return (1/n) * func(n, m+1)... else:... return n * func(n, m-1)...>>> func(3, 3)27>>> func(3, 1)3>>> func(3, 0)1>>> func(3, -1)0.3333333333333333>>> func(3, -2)0.1111111111111111>>> func(3, -3)0.037037037037037035
递归的特点:
必须有一个明确的结束条件;
每次进入更深一层递归时,问题的规模相比上次递归都应有所减少;
递归效率不高,递归层次太多,会导致栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的。每当进入一个函数调用,栈就会增加一层栈帧;每当函数返回,栈就会减少一层栈帧。由于栈的大小不是无限的,所以递归调用次数过多,会导致栈溢出。
递归的优缺点:
优点:
递归使代码看起来更加整洁、优雅;
可以用递归将复杂任务分解成更简单的子问题;
使用递归比使用一些嵌套迭代更容易。
缺点:
递归的逻辑很难调试、跟进;
递归调用的代价高昂(效率低),因为占用了大量的内存和时间。
注意:Python3默认递归的深度不能超过997层。
尾递归调用优化:
在函数的最后一步进入递归调用。这样每层函数的调用就直接结束了,不会去等待下一次递归调用的返回值,这样每次执行完毕进入下一次后,就不会占用内存去保存当前层的状态,节省内存空间。