1、函数执行流程
1.1、函数与栈关系
C语言中,函数的活动和栈有关。
栈是后进先出的数据结构。栈是由底端向顶端生长,栈顶加入数据称为压栈、入栈,栈顶弹出数据称为出栈。
1.2、流程示例
1.2.1、代码
def add(x, y):
r = x + y
print(r)
return r
def main():
a = 1
b = add(a, 2)
return b
main()
1.2.2、流程分析
main调用,在栈顶创建栈帧
a = 1,在main栈帧中增加a,堆里增加1,a指向这个1
b = add(a, 2),等式右边先执行,add函数调用
add调用,在栈顶创建栈帧,压在main栈帧上面
add栈帧中增加2个变量,x变量指向1,y指向堆中新的对象2
在堆中保存计算结果3,并在add栈帧中增加r指向3
print函数创建栈帧,实参r被压入print的栈帧中
print函数执行完毕,函数返回,移除栈帧
add函数返回,移除栈帧
main栈帧中增加b指向add函数的返回值对象
main函数返回,移除栈帧
1.2.3、问题点
如果再次调用main函数,和刚才的main函数调用,有什么关系?
每一次函数调用都会创建一个独立的栈帧入栈。
因此,可以得到这样一句不准确的话:哪怕是同一个函数两次调用,每一次调用都是独立的,这两次调用没什么关系。
2、递归
2.1、简介
函数直接或者间接调用自身就是 递归
递归需要有边界条件、递归前进段、递归返回段
递归一定要有边界条件
当边界条件不满足的时候,递归前进
当边界条件满足的时候,递归返回
2.2、斐波那契数列递归示例
斐波那契数列Fibonacci number:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ...
如果设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)
2.2.1、没有递归示例
def fib_v1(n): # n>=3
a = b = 1
for i in range(n-2):
a, b = b, a + b
return b
print(fib_v1(101))
print(fib_v1(35))
2.2.2、使用递归实现,需要使用上面的递推公式
# 递归
def fib_v2(n):
if n < 3:
return 1
return fib_v2(n-1) + fib_v2(n-2)
# 递归
def fib_v2(n):
return 1 if n < 3 else fib_v2(n-1) + fib_v2(n-2)
fib_v2(35) # 9227465
2.2.3、是执行fib(35)就已经非常慢了,为什么?
递归实现很美,但是执行fib(35)就已经非常慢了,为什么?
以fib(5)为例。看了下图后,fib(6)是怎样计算的呢?
这个函数进行了大量的重复计算,所以慢。

2.3、递归要求
递归一定要有退出条件,递归调用一定要执行到这个退出条件。没有退出条件的递归调用,就是无限调用
递归调用的深度不宜过深
Python对递归调用的深度做了限制,以保护解释器
超过递归深度限制,抛出RecursionError: maxinum recursion depth exceeded 超出最大深度
sys.getrecursionlimit()
2.4、递归效率
2.4.1、改进一下fib_v2函数
# 递归
def fib_v3(n, a=1, b=1):
if n < 3:
return b
a, b = b, a + b
#print(n, a, b)
return fib_v3(n-1, a, b) # 函数调用次数就成了循环次数,将上次的计算结果代入下次函数调用
fib_v3(101) # fib_v3(35)
2.4.2、小结
经过比较,发现fib_v3性能不错,和fib_v1循环版接近。但是递归函数有深度限制,函数调用开销较大。
2.5、间接递归
2.5.1、代码
def foo1():
foo2()
def foo2():
foo1()
foo1()
2.5.2、小结
间接递归调用,是函数通过别的函数调用了自己,这一样是递归。
只要是递归调用,不管是直接还是间接,都要注意边界返回问题。但是间接递归调用有时候是非常不明显,代码调用复杂时,很难发现出现了递归调用,这是非常危险的。
所有,使用良好的代码规范来避免这种递归的发生。
2.6、总结
递归是一种很自然的表达,符合逻辑思维
递归相对运行效率低,每一次调用函数都要开辟栈帧
递归有深度限制,如果递归层次太深,函数连续压栈,栈内存很快就溢出了
如果是有限次数的递归,可以使用递归调用,或者使用循环代替,循环代码稍微复杂一些,但是只
要不是死循环,可以多次迭代直至算出结果
绝大多数递归,都可以使用循环实现
即使递归代码很简洁,但是能不用则不用递归