递归
- 递归
- 基线条件和递归条件
- 调用栈
- 递归调用栈
递归
假设你在祖母的阁楼中翻箱倒柜,发现了一个上锁的神秘手提箱。钥匙很可能在下面这个盒子里。这个盒子里有盒子,盒子里的盒子又有盒子。钥匙就在某个盒子中。
方法一(循环):创建一个要查找的盒子堆,从盒子堆取出一个盒子,在里面找。如果找到的是盒子,加入盒子堆;如果找到钥匙,大功告成。
def look_for_key(main_box):
pile = main_box.make_a_pile_to_look_through()
while pile is not empty:
box = pile.grab_a_box()
for item in box:
if item.is_a_box():
pile.append(item)
elif item.is_a_key():
print "found the key!"
方法二(递归):函数调用自己。
def look_for_key(box):
for item in box:
if item.is_a_box():
look_for_key(item) # 递归!
elif item.is_a_key():
print "found the key!"
两种方法作用相同,但递归的解决方案更清晰。递归只是让解决方案更清晰,没有性能上的优势。实际上,在有些情况下使用循环的性能更好。
如果使用循环,程序的性能可能更高;如果使用递归,程序可能更容易理解。如何选择要看什么对你来说更重要。
基线条件和递归条件
编写递归函数时,很容易出错导致无限循环。每个递归函数都有两部分:
- 基线条件(base case):函数不再调用自己,避免无限循环
- 递归条件(recursive case):函数调用自己
def countdown(i):
print i
if i <= 0: # 基线条件
return
else: # 递归条件
countdown(i - 1)
调用栈
计算机在内部使用被称为调用栈的栈。栈是一种简单的数据结构,只有两种操作:压入(插入)和弹出(删除并读取)。
调用栈的工作原理:假设有一个函数 greet,它调用了 greet2 和 bye:
def greet(name):
print "hello, " + name + "!"
greet2(name)
print "getting ready to say bye..."
bye()
def greet2(name):
print "how are you, " + name + "?"
def bye():
print "ok bye!"
调用 greet("maggie") 时:
- 计算机为 greet 调用分配一块内存,存储变量 name 的值
- 打印 hello, maggie!,再调用 greet2("maggie")
- 计算机为 greet2 调用分配另一块内存,放在第一块上面(栈顶)
- 打印 how are you, maggie?,从 greet2 返回,栈顶内存块弹出
- 回到 greet,打印 getting ready to say bye...,调用 bye()
- bye 的内存块被压入栈顶
- 打印 ok bye!,从 bye 返回,弹出
- 回到 greet,没有别的事情,从 greet 返回
调用另一个函数时,当前函数暂停并处于未完成状态,该函数的所有变量的值都还在内存中。
递归调用栈
递归函数也使用调用栈。来看阶乘函数 fact(x),其中 5! = 5 * 4 * 3 * 2 * 1:
def fact(x):
if x == 1:
return 1
else:
return x * fact(x - 1)
调用 fact(3) 时,调用栈的变化:
- 调用 fact(3),但 fact(3) 需要 fact(2) 的结果,暂停
- 调用 fact(2),需要 fact(1) 的结果,暂停
- 调用 fact(1),返回 1
- fact(2) 恢复:2 * 1 = 2,返回 2
- fact(3) 恢复:3 * 2 = 6,返回 6
每个 fact 调用都有自己的 x 变量,在一个函数调用中不能访问另一个的 x 变量。
使用栈的代价:存储详尽的信息可能占用大量内存。每个函数调用都要占用一定的内存,如果栈很高,就意味着计算机存储了大量函数调用的信息。这时有两种选择:
- 重新编写代码,转而使用循环
- 使用尾递归(高级递归主题,并非所有语言都支持)
小结
- 递归指的是调用自己的函数
- 每个递归函数都有两个条件:基线条件和递归条件
- 栈有两种操作:压入和弹出
- 所有函数调用都进入调用栈
- 调用栈可能很长,这将占用大量的内存