函数执行流程
要明白递归首先需要明白函数的执行流程。
比如下面的这段程序。
def foo1(b, b1=3):print("foo1 called", b, b1)deffoo2(c):
foo3(c)print("foo2 called", c)deffoo3(d):print("foo3 called", d)defmain():print("main called")
foo1(100, 101)
foo2(200)print("main ending")
main()
结果为:
main called
foo1 called100 101foo3 called200foo2 called200main ending
上面的代码执行流程为:
首先在全局帧中生成foo1、foo2、foo3、main函数对象。
然后main函数调用。
main函数中查找内建函数print压栈,然后将常量字符串压栈,然后调用函数,弹出栈顶。
main函数中全局查找函数foo1压栈,然后将常量100,101压栈,调用函数foo1,创建栈帧。然后字符串和变量b,b1压栈,调用函数,弹出栈顶,返回值。
main函数中全局查找函数foo2函数压栈,然后将常量200压栈,调用foo2,创建栈帧。foo3函数压栈,变量c引用压栈,调用foo3,创建栈帧。foo3完成print函数调用后返回。foo2恢复调用,执行print后,返回值。main中foo2调用结束弹出栈顶,main继续执行print函数调用,弹出栈顶,main函数返回。
递归
所谓的递归也就是函数直接或者间接调用自身。需要注意的是递归一定要有边界条件(不然就就是死循环),递归前进段、递归返回段,当边界条件不满足的时候,递归前进,而当递归条件满足的时候,递归返回。
递归相比于循环,只是让解决方案更加清晰,并没有性能上的优势。在有些情况下,如果使用循环,程序的性能可能会更高,而如果使用递归的话,程序可能更容易理解。
比如要生成一个斐波拉契数列,1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ... ߧ如果设f(n)为该数列的第n项,那么可以推导出这样的公式:f(n)=f(n-1)+f(n-2).
使用循环,生成斐波拉契数列,代码为:
pre =0
cur= 1#第一个
print(pre,cur,end=" ")
n= 4
for i in range(n-1):
pre,cur= cur,pre+curprint(cur,end = " ")
结果为:
01 1 2 3将它抽象出函数:deffib(n):
pre=0
cur= 1
print(cur,end=" ")for i in range(n-1):
pre,cur= cur,pre+curprint(cur,end = " ")
fib(10)
结果为:1 1 2 3 5 8 13 21 34 55
而如果使用递归的话,代码可以很简洁:
#第n项的数字为:
deffib(n):if n<2:return 1
else:return fib(n-1)+fib(n-2)
fib(10)
上面的代码可以简化为:deffib(n):return 1 if n<2 else fib(n-1)+fib(n-2)
fib(10)
而生成数列的代码为:
deffib(n):return 1 if n <2 else fib(n-1) + fib(n-2)print(fib(10))for i in range(11):print(fib(i), end=' ')
结果为:89
1 1 2 3 5 8 13 21 34 55 89
由上面的例子可以看到,递归一定要有退出条件,而递归调用一定会执行到这个退出条件,如果没有退出条件的递归调用,就是无限调用,会陷入无限循环,而且递归调用的深度不宜过深,Python对递归调用的深度做了限制(1000)以保护解释器,超过递归深度的限制,会抛出recursionerror错误,同时可以使用sys.getrecursionlimil()设置递归的深度。
递归的性能
循环稍微复杂一些,但是只要不是死循环,就可以多次迭代算出结果,递归函数的代码极简易懂,但是只能获取到最外层的函数调用,内部的递归结果都是中间结果,而且给定一个N都要进行近2N次递归,深度越深,效率就会越低,为了获取斐波拉契数列需要再在外面套一个n的循环,效率就更低了。如果递归复杂,函数会反复的压栈,这样栈的内存很快就会溢出。所以会有递归深度限制。如下面的代码:
importdatetime
start=datetime.datetime.now()
pre=0
cur= 1 #No1
print(pre, cur, end=' ')
n= 35
for i in range(n-1):
pre, cur= cur, pre +curprint(cur, end=' ')
delta= (datetime.datetime.now() -start).total_seconds()print(delta)
结果为:
01 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765 10946 17711 28657 46368 75025 121393 196418 317811 514229 832040 1346269 2178309 3524578 5702887 9227465 0.001
importdatetime
n= 35start=datetime.datetime.now()deffib(n):return 1 if n < 2 else fib(n-1) + fib(n-2)for i inrange(n):print(fib(i), end=' ')
delta= (datetime.datetime.now() -start).total_seconds()print(delta)
结果为:1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765 10946 17711 28657 46368 75025 121393 196418 317811 514229 832040 1346269 2178309 3524578 5702887 9227465 9.371536
上面的例子可以看到for循环,只需要0.01秒就可以很快算出35个斐波拉契数列,而递归的话则需要9秒多。效率特别低,这个时候有什么可以改进的吗?
pre =0
cur= 1#第一个
print(pre,cur,end=" ")def fib(n,pre = 0,cur = 1):
pre,cur= cur,pre+curprint(cur,end=" ")if n ==2:returnfib(n-1,pre,cur)
fib(10)
结果为:
01 1 2 3 5 8 13 21 34 55
上面的这个递归函数就和循环的思想很类似了,参数n是边界条件,用n来计数,上一次的计算结果直接用作函数的实参,这样的话,效率更高,和循环相比较的话,性能更近。
所以递归并不一样都是效率低下,但是它有深度限制。
可以对比下三个斐波拉契数列生成函数的效率。如下:
importdatetime#Fib Seq
start =datetime.datetime.now()
pre=0
cur= 1 #No1
print(pre, cur, end=' ')
n= 35
#loop
for i in range(n-1):
pre, cur= cur, pre +curprint(cur, end=' ')
delta= (datetime.datetime.now() -start).total_seconds()print(delta)#Fib Seq
start =datetime.datetime.now()
pre=0
cur= 1 #No1
print(pre, cur, end=' ')#recursion
def fib1(n, pre=0,cur=1):
pre, cur= cur, pre +curprint(cur, end=' ')if n == 2:returnfib1(n-1, pre, cur)
fib1(35)
delta= (datetime.datetime.now() -start).total_seconds()print(delta)
start=datetime.datetime.now()deffib2(n):if n < 2:return 1
return fib2(n-1) + fib2(n-2)for i inrange(n):print(fib2(i), end=' ')
delta= (datetime.datetime.now() -start).total_seconds()print(delta)
前面两个性能相当,而第三个效率特特别低下。写递归应该尽量避免写成第三个这样的。
间接递归
所谓的间接递归,就是通过别的函数间接调用了函数自身,但是,如果构成了循环递归调用就是件非常危险的事情,而且这种情况在代码复杂的情况下, 很还可能发生。所以一定要规范代码,来避免发生这种调用的发生。比如下面这个例子。
deffoo1():
foo2()deffoo2():
foo1()
foo1()
上面的代码就会陷入死循环。应该尽量的避免。
递归总结:
递归是一种很自然的表达,符合逻辑思维。
递归相对来讲运行效率低,每一次调用函数都要开辟栈帧。
递归有深度限制,如果递归的层次太深,函数反复的压栈,栈内存很快就会溢出。
如果是有限次的递归,可以使用递归调用,或者使用循环代码,循环代码稍微复杂一些,但是只要不是死循环,可以多次迭代直至算出结果。
绝大多数递归,都可以使用循环来实现。
即便递归代码简洁,但是能不用则不用递归。
练习题:
求n的阶乘!
将一个数逆序放入列表中,比如1234--[4,3,2,1]
解决猴子吃桃问题:猴子第一天摘下若干个桃子,马上吃了一半,还不过瘾,又多吃了一个,第二天早上又将剩下的桃子吃掉了一半,又多吃了一个。以后每天早上都吃了前一天剩下的一半零一个。到第10天早上想吃的时候,只剩下一个桃子,求第一天共摘了多少个桃子。
#求阶乘
def fac(n):#性能最好
if n==1:return 1
else:return n*fac(n-1)
fac(10)
结果为:3628800
def fac1(n,p=1):if n==1:returnp
p*=nprint(p)
fac1(n-1,p)returnp
fac1(10)
结果为:10
90
720
5040
30240
151200
604800
1814400
3628800
10
def fac2(n,p=None):if p isNone:
p= [1]if n == 1:returnp[0]
p[0]*=n#print(p[0])
fac2(n-1,p)returnp[0]
fac2(10)
结果为:3628800
def fac3(n,p=1):
p*=nif n==1:returnpelse:return fac4(n-1,p)
fac3(10)
结果为:3628800
2.将一个数逆序放入列表中
data = str(1234)defrevert(x):if x==-1:return " "
return data[x]+revert(x-1)print(revert(len(data)-1))
结果为:4321
def revert1(n,lst =None):if lst isNone:
lst=[]
lst.append(n%10)if n//10 ==0:returnlstreturn revert1(n//10,lst)
revert1(12345)
结果为:
[5, 4, 3, 2, 1]def revert2(n,lst =None):if lst isNone:
lst=[]
x,y=divmod(n,10)
lst.append(y)if x==0:returnlstreturnrevert2(x,lst)
revert2(12345)
结果为:
[5, 4, 3, 2, 1]
num= 12354
def revert3(num,target=[]):ifnum:
target.append(num[len(num)-1])#target.append(num[-1:])
revert3(num[:len(num)-1])returntargetprint(revert3(str(num)))
结果为:
['4', '5', '3', '2', '1']
猴子吃桃问题:
def peach(day= 9,sum = 1):
sum=2*(sum+1)
day-=1
if day ==0:returnsumreturnpeach(day,sum)print(peach())
结果为:1534
defmonkey(n):if n==1:return 1
return 2*monkey(n-1)+2monkey(10)
结果为:1534
这个猴子吃桃的问题,可以假设猴子摘了x个桃子
d1: x//2-1
d2:d1//2-1
d3:d2//2-1
……
d9:d8//2-1
d10:1
那么:
def peach(day = 10):if day==1:return 1
return (peach(day-1)+1)*2
print(peach())
结果为:1534
应该注意的是,这里必须是10,因为return (peach(day-1)+1)*2立即拿不到结果,必须通过再一次进入函数时判断是不是到了最后一天。也就是当前使用的值是由下一次函数调用得到的,所以要执行10次函数调用。换一种方式表达:
def peach(days = 1):if days==10:return 1
return (peach(days+1)+1)*2
print(peach())
结果为:1534