递归
什么是递归
迭代的是人,递归的是神 —— L.Peter Deutsch
简单定义:
当函数直接或者简介调用自己的时候,发生递归。
基本要素
- 边界条件:确定递归到何时终止,也称为递归出口
- 递归模式:大问题如何分解成小问题的,也成为递归体
举个栗子:
# 计算阶乘
def func(n):
nub = 1
while n != 1:
nub *= n
n -= 1
else:
return nub
print(func(5))
用递归实现呢
def func(n):
if n == 1:
return n
return n * func(n - 1)
第一种是平常使用的迭代的思想:当n不等于1时,每次循环 乘 ,全部计算结束以后在返回值
而递归,因为 func(N) = N* func(N-1).换句话说,为了获得 func(N)的值,需要计算 func(N-1).而且,为了找到 func(N-1),需要计算func(N-2)等等。计算到func(1)的时候拿到结果在一次计算func(2)… func(N)
理解递归:
在初识递归的时候,我们经常会陷入不停的回溯验证之中,因为回溯验证就像反过来思考迭代,这是我们习惯的思考方式,但是对于递归,我们并不需要这样验证。比如上面的阶乘计算:
用回溯的方式思考:比如 n = 4 那么func(4) 等于 4 * func(3) 而 func(3) 等于 3 * func(2),func(2) 等于 2 * func(1), 而func(1) 等于1 所以func(4) = 4 * 3 * 2 * 1 这个结果整好是等于阶乘4的迭代定义
但是这种方法对于简单的还好 ,如果是复杂的就可以GG, 而且还无益于理解
Paul Graham提到一种方法,如下:
当n=0,1的时候,结果正确(你必须要示范如何解决问题的一般情况, 通过将问题切分成有限小并更小的子问题)
假设函数对于n是正确的,函数对n+1的结果也是正确的
如果这两点成立,我们知道这个函数对于所有可能的n都是正确的
(你必须要示范如何通过有限的步骤, 来解决最小的问题(基本用例).如果这两件事完成了, 那问题就解决了. 因为递归每次都将问题变得更小, 而一个有限的问题终究会被解决的, 而最小的问题仅需几个有限的步骤就能解决.)
这种方法很像 数学归纳法, 也是递归正确的思考方式, 事实上, 阶乘的递归表达方式就是1!=1,n!=(n-1)!×n
(见wiki). 当程序实现符合算法描述的时候, 程序自然对了, 假如还不对, 那是算法本身错了…… 相对来说, n,n+1的情况为通用情况, 虽然比较复杂, 但是还能理解, 最重要的, 也是最容易被新手忽略的问题在于第1点, 也就是基本用例(base case)要对. 比如, 上例中, 我们去掉if n <= 1
的判断后, 代码会进入死循环, 永远不会结束.
当函数被调用时,它的变量的空间是创建于运行时堆栈上的。以前调用的函数的变量扔保留在堆栈上,但他们被新函数的变量所掩盖,因此是不能被访问的。 当递归函数调用自身时,情况于是如此。每进行一次新的调用,都将创建一批变量,他们将掩盖递归函数前一次调用所创建的变量。当我追踪一个递归函数的执行过程时,必须把分数不同次调用的变量区分开来,以避免混淆。
递归的优缺点:
优点:
会让代码变简单
缺点:
占用内存(解决方法尾递归)
什么时候使用递归
当问题可用递归来解决所需要具备的条件
- 子问题与原问题为同样的事,而且规模更小
- 程序具有停止的条件
递归的实例
例一 汉诺塔
有三根杆子A,B,C。A杆上有N个(N>1)穿孔圆盘,盘的尺寸由下到上依次变小。要求按下列规则将所有圆盘移至C杆:
1.每次只能移动一个圆盘.
2.大盘不能叠在小盘上面.
思路:假设 现在我们一共在A杆 上 一共 有3 个盘子,我们要想把最大的盘子放到C杆 上,我们需要把除去最大的一个盘子(n-1) 全部放到B杆上,然后将最大的盘子放到C杆上,然后在将其余的通过A杆放到C杆上。
def move(n, a, buffer, c, count = [0,]):
# n : 杆上有多少个盘
# a : 目标所在杆
# buffer : 过程杆
# c : 目标杆
# count : 通过默认参数是可变数据类型时公用同一个内存空间,存放走的步数
if(n == 1):#
count[0] += 1
print('第',count,'步',a,"->",c)
return
move(n-1, a, c, buffer, count)# 将除最大的盘之外的所有盘 从a 通过c 放到b
move(1, a, buffer, c, count) # 将最大盘 从a 通过b 放到c
move(n-1, buffer, a, c, count)# 将除最大的盘之外的所有盘 从b 通过a 放到c
move(3, "a", "b", "c")
例二 上楼梯
有个小孩正在上楼梯,楼梯有n阶台阶,小孩一次可以上1阶、2阶。计算小孩上楼梯的方式有多少种
# 1 1
# 2 2
# 3 3
# 4 5
# 5 8
由规律可知这个是一个变形的斐波那契数列
def func(n, a = 1, b = 2):
if n == 1:
return a
return func(n-1, b, a+b)
例三 上楼梯
有个小孩正在上楼梯,楼梯有n阶台阶,小孩一次可以上1阶、2阶、3阶。计算小孩上楼梯的方式有多少种
思路:由于最开始查数差错了,导致没找到任何规律 我们将本题想象成下楼梯,当阶梯为0的时候算下完楼梯, 下楼梯有3种方案,我们可以下一层,也可以2、3层,只要下面的楼梯足够,我们有3种选择,而不是三选一 可能有点难理解但是想表达的意思是 代码中 使用三个if 而不是 if-elif-elif 的意思
也可以套用例一的思想,我下一层后,让第二个人告诉我要下几层台阶
def func(n, l=[0]):
if n == 0:
l[0] += 1
return
if n >= 1:
func(n-1, l)
if n >=2:
func(n-2, l)
if n >= 3:
func(n-3, l)
return l[0]
print(func(5))
例四 二分法查找
L = [1,2,3,4,5]
def func(L, aim, start = 0, end = None):
end = len(L) if end is None else end
min_index = (end - start) // 2 + start
if end <= start: return -1
start, end = (min_index + 1, end) if L[min_index] < aim else (start, min_index - 1)
if L[min_index] == aim:
return min_index
return func(L, aim, start, end)
例五 n层菜单
这个是凑数的
menu = {
'北京': {
'海淀': {
'五道口': {
'soho': {},
'网易': {},
'google': {}
},
'中关村': {
'爱奇艺': {},
'汽车之家': {},
'youku': {},
},
'上地': {
'百度': {},
},
},
'昌平': {
'沙河': {
'老男孩': {},
'北航': {},
},
'天通苑': {},
'回龙观': {},
},
'朝阳': {},
'东城': {},
},
'上海': {
'闵行': {
"人民广场": {
'炸鸡店': {}
}
},
'闸北': {
'火车战': {
'携程': {}
}
},
'浦东': {},
},
'山东': {},
}
def threeLM(dic):
while True:
for k in dic:print(k)
key = input('input>>').strip()
if key == 'b' or key == 'q':return key
elif key in dic.keys() and dic[key]:
ret = threeLM(dic[key])
if ret == 'q': return 'q'
elif (not dic.get(key)) or (not dic[key]) :
continue
threeLM(menu)