在前面的课程中,我们深入介绍了如何创建和调用函数。函数可以调用其他函数,但可能让你感到惊讶的是,函数还可以调用自己。
递归是怎么定义的呢?
递归:参见"递归”
这种解释挺蠢的,我们再来看一段代码,它是一个函数调用自身的实例
def recursion():
print(1)
recursion()
recursion()
# RecursionError: maximum recursion depth exceeded while calling a Python object
该函数没有任何意义,只是在自身调用自身。当我们运行它,一段时间之后,这个程序崩溃了。。。
理论上来说,这个函数会一直运行下去,但每次函数执行时都会消耗一些内存,创建一个新的命名空间,因此当函数的调用次数达到一定程度并且之前的函数没有返回(终止)时,就会导致程序终止并提示错误信息"超过最大递归层级"。
这个函数的递归可以称为无穷递归,因为它理论上永远不会停止,相比无限循环,对我们编程的帮助更少。。。那我们该如何写出一个能对我们有所帮助的递归呢?
递归和递推是计算机中解决问题时常用的方法,通过对比,我们探究一下递归的使用
先来看一个例子:
假设你要通过一个函数求出n的阶乘,也就是1*2*3*4…*(n-1)*n的值,你该怎么做呢?
按照一般的思路,我们可以这样思考:
因此我们不妨按照规律,从1一直乘到边界n
那么,函数就呼之欲出了
def get_res(n):
result = 1
for i in range(1,n+1):
result *= i
return result
get_res(3)
# 6
其实,这种思考问题的方法就是递推。
递推是人本能的正向思维,我们小时候学习数数,从1,2,3一直数到100,就是典型的递推。类似地,我们在学习过程中循序渐进,出发点都是这样正向,由易到难,由小到大,由局部到整体,从1乘到100,这在生活中这种做法不仅合情合理,而且是天然而成的,我们从来不觉得它有什么问题。
简单的总结,递推其实是在已知边界值的情况下按照规律进行推导
但是上面的问题要交给计算机,计算机是怎么算的呢?
它是倒着来的。比如要算5!,计算机就把它变成5x4!(五乘以四的阶乘)。当然,你会说,4!还不知道呢。没关系,计算机会说,采用同样的方法,把它变成4x3!。至于3!,则用同样的算法处理。最后做到1!时,计算机知道了它就等于自己,即1!=1,从此不再往下扩展了。接下来,就是倒推回所有的结果,由于知道了1!,2!,然后3!,4!,5!就统统都知道了。
这种思路就是递归。相比于递推的"由小及大",递归更像是"由大及小"。我们再来看一看这道题,由大及小的分析一下
只要有了最小问题的解(基线条件),和通解(递归条件)那么我们就可以动手写函数了
def get_res(n):
if n == 1:
return 1 # 如果求1的阶乘,直接给他
return n * get_res(n-1) # 如果求大于1的n的阶乘,给他n-1的阶乘乘以n
get_res(3)
# 6
是不是很惊喜?我们很清晰明了,并且方便快捷的解决了问题。
如果还原get_res(5)
的运算过程,应该是这样的
===> get_res(5) # 返回值还不是终值,依次调用 依次入栈
===> 5 * get_res(4)
===> 5 * (4 * get_res(3))
===> 5 * (4 * (3 * get_res(2)))
===> 5 * (4 * (3 * (2 * get_res(1)))) # 到达基线条件 先入后出,依次出栈进行计算
===> 5 * (4 * (3 * (2 * 1)))
===> 5 * (4 * (3 * 2))
===> 5 * (4 * 6)
===> 5 * 24
===> 120
递归的过程其实也名如其意,递出去,再还回来。每一次的函数调用都要入栈,直到基线条件,依次出栈。
简单的总结,递归其实是知道终点(基线条件)的情况下,由大及小的对问题进行降解。
我们在上文中用到了栈的概念,也提到了先进后出,其实可以把栈看作一种存储数据的方式,类似我们吃饭的饭盒,先放进去的菜在最下面,最后才吃得到
使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。可以试试
get_res(1000)
其实理解了递归和递推,我们解决问题的思路就会宽阔很多。在来一个问题:
求 x 的 n 次幂 x非0,n为整数
递推的思路:
代码:
def get_res(x,n):
result = 1
for i in range(0,n):
result *= x
return result
换做递归呢?思路同样也简单:
代码:
def get_res(x,n):
if n == 0:
return 1
else:
return get_res(x,n-1)*x
在很多情况下,因为不需要重复的入栈,递推的效率会比递归高,然而很明显,递归的可读性更好一些。
递归的一个经典案例就是二分查找。二分查找是程序中很常用的一种算法。
想象你对面的人心中有一个1~100的数字,你要猜出是哪个,当然,猜100次肯定对,但最少要猜多少次呢?其实我们没有必要每次都给一个准确的数字,只需要不断的缩小范围即可。
首先问:“这个数字大于50吗”,如果是肯定的,那么继续提问"这个数字大于75吗?",不断的将区间减半,直到猜对为止。
对于列表也是一样,对于给定上下区间的列表,列表元素为递增的值,每次寻找某值,只需要不断的将范围减半,就能很快的查找到。如果使用递归,那么基线条件就已经确定了: “区间上限=区间下限”
思路:
代码:
def search(mylist,number,lower=0,upper=None): # 给出默认值
if upper is None: # 如果没有上限,上限是最大索引值
upper = len(mylist)-1
if lower == upper: # 如果上下限相等,直接找到位置
return lower
else:
middle = (lower + upper) // 2 # 否则二分查找
if number > mylist[middle]:
return search(mylist,number,middle+1,upper)
else:
return search(mylist,number,lower,middle)
seq = [34, 67, 8, 123, 4, 100, 95]
seq.sort()
print(search(seq,100))
# 6
这样写的好处在哪里呢?
如果我们用递推去寻找,方法很简单,只需要使用for循环遍历每一个列表元素并比较即可
seq = [34, 67, 8, 123, 4, 100, 95]
def search(mylist,number):
for val in mylist:
if val == number:
return mylist.index(number)
print(search(seq,100))
# 5
虽然递推看上去更简单了一些,但是从查找次数上来说,如果寻找100个数,而恰巧找的是100,递推必须要推到100次才能找到,而递归,只需要7次。二分查找的效率非常高。