函数直接或者间接调用自身就是递归。递归需要有边界条件,递归前进段、递归返回段。递归一定要有边界条件。当边界条件不满足的时候,递归前进。当边界条件满足的时候,退出递归。
举例,斐波那契数列:
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233……
先前的一般做法是利用循环:
a = 0
b = 1
n = 10
for i in range(n - 1):
a, b = b, a + b
else:
print(b)
如果设 F(n) 为第 n 项 (n ∈ N*),那么斐波那契数列可以写成:F(n) = F(n-1) + F(n-2)。如果再设置F(0) = 0,,F(1) = 1,那么通过F(n) = F(n-1) + F(n-2) 就可以算出任意项,这就是递归方法:
def fib(n):
return 1 if n < 3 else fib(n-1) + fib(n-2)
一、递归的要求
递归一定要有退出条件,递归调用一定要执行到这个条件退出。没有退出条件的递归调用,就是无限调用
-
调用的深度不宜过深
- python 对递归调用做了限制,以保护解释器
- 超过递归深度,抛出 RecursionError: maximum recursion depth exceeded
- sys.getrecursionlimit() 可以查看限制的次数,默认是1000,不建议修改
二、递归的性能
将上面的斐波那契数列的两种方法进行对比,得到以下结果:
- 循环的代码复杂,但是只要不是死循环,可以进行多次迭代直至算出结果
- 递归的代码简单,但是只能获取到最外层的函数调用,内部函数递归结果都是中间结果。而且给定一个n都要进行 近2n次 的递归调用,深度越深,效率越低,为了获取整个斐波那契数列数列而不是单项数字,还得要在外面再套一层循环才行,效率更低了。
- 递归还有深度限制,如果递归复杂,函数反复压栈,栈内存很快溢出
三、提高递归的性能
改进后的递归代码:
def fib(n, a=0, b=1):
a, b = b, a+b
if n == 1:
return a
else:
fib(n-1)
解析:
- 与循环的思路类似
- 参数n是边界条件,用n计数
- 上一次的计算结果直接作为函数的实参
- 效率很高,和循环相比,性能相近,这是因为递归的次数接近减半了。所以说不是说递归的性能不好,只是递归有深度限制
四、间接递归
def foo1():
foo2()
def foo2():
foo1()
foo1()
间接递归,就是通过其他函数来调用自身。但是,构成循环递归调用是很危险的,但是往往在代码复杂的时候会出现这种情况,要避免这种情况的发生。
五、总结
- 递归是一种很自然的表达,符合逻辑思维
- 递归的效率相对运行效率低,因为没调用一次函数都要开辟栈帧
- 递归有深度限制,如果递归层次太深,函数反复压栈,栈内存很快就溢出了
- 如果是有限次的递归,可以使用递归,或者使用循环代替,循环的代码稍微复杂,但是只要不是死循环,可以迭代多次直至算出结果
- 绝大多数递归都可以使用循环解决
- 即是代码可以简化,但是能不用递归就不要用
六、递归练习题
关于递归的解法,一般有两种:
一种如同数学公式
一种类似循环,相当于循环的的改版,将循环迭代,变成函数调用的压栈
习题1:求n的阶乘
# 阶乘公式
def factorial(n):
if n < 2:
return 1
else:
return factorial(n-1) * n
# 循环完成
n = 5
fac = 1
for i in range(n, 0, -1):
fac = fac * i
print(fac)
# 循环变递归
def factorial(n, fac=1):
fac = fac * n # 循环体
if n < 2: # 边界
return fac
else:
return factorial(n-1, fac) # 进行下一次函数调用,通过参数将每一次的循环体往下传递
习题2:解决猴子吃桃问题
猴子第一天摘下若干个桃子,当即吃掉一半,还不过瘾,有多吃了一个。第二天早上又将剩下的桃子吃去一半,又多吃了一个。以后每天早上都吃了前一天剩下的一半零一个。到第10天早上想吃时,只剩下一个桃子了。问一开始摘了多少个桃子。
# 递归
def f(n=1):
if n == 10:
return 1
else:
return (f(n-1) + 1) * 2
print(fn)
# 循环
peach = 1
for i in range(1, 10):
peach = (peach + 1) * 2
print(peach)
# 循环改递归
def peach(days=10, p=1):
if days == 1:
return p
p = (p + 1) * 2
return peach(days-1, p)
print(peach())
习题三:将一个数逆放入列表中
例如1234 ==> [4, 3, 2, 1],一个数字1234被切片后,变成了4项,逆序放在了列表中
# 字符串切片
data = str(1234)
def reverse(data):
if not data:
return []
return [data[-1]] + reverse(data[:-1])
print(reverse(data))
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def reverse01(data, newdata=[]):
newdata = newdata + [data[-1]]
if not data:
return newdata
return reverse(data[:-1])
print(reverse(data))
# 使用整数整除取模
data = 1234
def revert(data, target=None):
if target is None: # 一开始就创建空列表作为容器
target = []
x, y = divmod(data, 10) # divmod(x, y) ==> Return the tuple (x//y, x%y)
target.append(y) # 存储个位数
if x == 0: # 边界
return target
return revert(x, target) # 每一次迭代都会取出data里的一位,将少了一位的data和容器继续往下传
print(revert(data))