递归函数
1.递归函数
在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。
递归函数特性:
- 必须有一个明确的结束条件;
- 每次进入更深一层递归时,问题规模相比上次递归都应有所减少
- 相邻两次重复之间有紧密的联系,前一次要为后一次做准备(通常前一次的输出就作为后一次的输入)。
- 递归效率不高,递归层次过多会导致栈溢出(在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出)
先举个简单的例子:计算1到100之间相加之和;通过循环和递归两种方式实现
-
# 循环方式
-
def sum_cycle(n):
-
sum = 0
-
for i in range(1,n+1) :
-
sum += i print(sum)
-
-
# 递归方式
-
def sum_recu(n):
-
if n>0:
-
return n +sum_recu(n-1)
-
else:
-
return 0
-
-
sum_cycle(100)
-
sum = sum_recu(100) print(sum)
结果:
5050
5050
递归函数的优点是定义简单,逻辑清晰。理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。
***使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。
把上面的递归求和函数的参数改成10000就导致栈溢出!
RecursionError: maximum recursion depth exceeded in comparison
**解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。
一般递归
-
def normal_recursion(n):
-
if n == 1:
-
return 1
-
else:
-
return n + normal_recursion(n-1)
执行:
-
normal_recursion(5)
-
5 + normal_recursion(4)
-
5 + 4 + normal_recursion(3)
-
5 + 4 + 3 + normal_recursion(2)
-
5 + 4 + 3 + 2 + normal_recursion(1)
-
5 + 4 + 3 + 3
-
5 + 4 + 6
-
5 + 10
-
15
可以看到, 一般递归, 每一级递归都需要调用函数, 会创建新的栈,随着递归深度的增加, 创建的栈越来越多, 造成爆栈:boom:
尾递归(http://www.open-open.com/lib/view/open1480494663229.html)
尾递归基于函数的尾调用, 每一级调用直接返回函数的返回值更新调用栈,而不用创建新的调用栈, 类似迭代的实现, 时间和空间上均优化了一般递归!
-
def tail_recursion(n, total=0):
-
if n == 0:
-
return total
-
else:
-
return tail_recursion(n-1, total+n)
执行:
-
tail_recursion(5)
-
tail_recursion(4, 5)
-
tail_recursion(3, 9)
-
tail_recursion(2, 12)
-
tail_recursion(1, 14)
-
tail_recursion(0, 15)
-
15
可以看到, 每一级递归的函数调用变成"线性"的形式.
深入理解尾递归
呃, 所以呢? 是不是感觉还不够过瘾... 谁说尾递归调用就不用创建新的栈呢?
还是让我们去底层一探究竟吧
-
int tail_recursion(int n, int total) {
-
if (n == 0) {
-
return total;
-
}
-
else {
-
return tail_recursion(n-1, total+n);
-
}
-
}
-
-
int main(void) {
-
int total = 0, n = 4;
-
tail_recursion(n, total);
-
return 0;
-
}
反汇编
-
$ gcc -S tail_recursion.c -o normal_recursion.S
-
$ gcc -S -O2 tail_recursion.c -o tail_recursion.S gcc开启尾递归优化
对比反汇编代码如下(AT&T语法)
可以看到, 开启尾递归优化前, 使用call调用函数, 创建了新的调用栈(LBB0_3);
而开启尾递归优化后, 就没有新的调用栈生成了, 而是直接pop
bp指向的 _tail_recursion 函数的地址(pushq %rbp)然后返回,
仍旧用的是同一个调用栈!
存在的问题
虽然尾递归优化很好, 但python 不支持尾递归,递归深度超过1000时会报错
一个牛人想出的解决办法
实现一个 tail_call_optimized 装饰器
-
#!/usr/bin/env python2.4
-
# This program shows off a python decorator(
-
# which implements tail call optimization. It
-
# does this by throwing an exception if it is
-
# it's own grandparent, and catching such
-
# exceptions to recall the stack.
-
-
import sys
-
-
class TailRecurseException:
-
def __init__(self, args, kwargs):
-
self.args = args
-
self.kwargs = kwargs
-
-
def tail_call_optimized(g):
-
"""
-
This function decorates a function with tail call
-
optimization. It does this by throwing an exception
-
if it is it's own grandparent, and catching such
-
exceptions to fake the tail call optimization.
-
-
This function fails if the decorated
-
function recurses in a non-tail context.
-
"""
-
def func(*args, **kwargs):
-
f = sys._getframe()
-
# 为什么是grandparent, 函数默认的第一层递归是父调用,
-
# 对于尾递归, 不希望产生新的函数调用(即:祖父调用),
-
# 所以这里抛出异常, 拿到参数, 退出被修饰函数的递归调用栈!(后面有动图分析)
-
if f.f_back and f.f_back.f_back \
-
and f.f_back.f_back.f_code == f.f_code:
-
# 抛出异常
-
raise TailRecurseException(args, kwargs)
-
else:
-
while 1:
-
try:
-
return g(*args, **kwargs)
-
except TailRecurseException, e:
-
# 捕获异常, 拿到参数, 退出被修饰函数的递归调用栈
-
args = e.args
-
kwargs = e.kwargs
-
func.__doc__ = g.__doc__
-
return func
-
-
-
def factorial(n, acc=1):
-
"calculate a factorial"
-
if n == 0:
-
return acc
-
return factorial(n-1, n*acc)
-
-
print factorial(10000)
为了更清晰的展示开启尾递归优化前、后调用栈的变化和tail_call_optimized装饰器抛异常退出递归调用栈的作用, 我这里利用 pudb调试工具 做了动图 <br/>
开启尾递归优化前的调用栈
开启尾递归优化后(tail_call_optimized装饰器)的调用栈
通过pudb右边栏的stack, 可以很清晰的看到调用栈的变化.
因为尾递归没有调用栈的嵌套, 所以Python也不会报 RuntimeError: maximum recursion depth exceeded 错误了!
这里解释一下 sys._getframe() 函数:
-
sys._getframe([depth]):
-
Return a frame object from the call stack.
-
If optional integer depth is given, return the frame object that many calls below the top of the stack.
-
If that is deeper than the call stack, ValueEfror is raised. The default for depth is zero,
-
returning the frame at the top of the call stack.
-
-
即返回depth深度调用的栈帧对象.
-
-
import sys
-
-
def get_cur_info():
-
print sys._getframe().f_code.co_filename # 当前文件名
-
print sys._getframe().f_code.co_name # 当前函数名
-
print sys._getframe().f_lineno # 当前行号
-
print sys._getframe().f_back # 调用者的帧
补充
二分法查找大家应该听说过;就是一种快速查找的方法,时间复杂度低,逻辑简单易懂,总的来说就是不断的找出中间值,用中间值对比你需要找的实际值;若中间值大,则继续找左边;若中间值小,则继续找右边;可以看出二分法就是不断重复此上过程,所以就可以通过递归方式来实现二分法查找了!
-
#The binary search function
-
-
def Binary_Search(data_source,find_n):
-
#判断列表长度是否大于1,小于1就是一个值
-
if len(data_source) >= 1:
-
#获取列表中间索引;奇数长度列表长度除以2会得到小数,通过int将转换整型
-
mid = int(len(data_source)/2)
-
#判断查找值是否超出最大值
-
if find_n > data_source[-1]:
-
print('{}查找值不存在!'.format(find_n))
-
exit()
-
#判断查找值是否超出最小值
-
elif find_n < data_source[0]:
-
print('{}查找值不存在!'.format(find_n))
-
exit()
-
#判断列表中间值是否大于查找值
-
if data_source[mid] > find_n:
-
print('查找值在 {} 左边'.format(data_source[mid]))
-
#调用自己,并将中间值左边所有元素做参数
-
Binary_Search(data_source[:mid],find_n)
-
#判断列表中间值是否小于查找值
-
elif data_source[mid] < find_n:
-
#print('查找值在 {} 右边'.format(data_source[mid]))
-
#调用自己,并将中间值右边所有元素做参数
-
Binary_Search(data_source[mid:],find_n)
-
else:
-
#找到查找值
-
print('找到查找值',data_source[mid])
-
else:
-
#特殊情况,返回查找不到
-
print('{}查找值不存在!'.format(find_n))
-
-
Data = [22,12,41,99,101,323,1009,232,887,97]
-
#列表从小到大排序
-
Data.sort()
-
#查找323
-
Binary_Search(Data,323)
-
-
执行结果:
-
找到查找值 323
总结:
递归分为两个过程:1.回溯 2.递推 但是递归函数中最重要的一点是就是结束条件,结束条件关系着你的递归函数能不能写出来。 递归的本质:自己调用自己(可以直接调用也可以间接调用)