[Python系列] Python函数及函数式编程(六)

第五节 递归函数

在前面的课程中,我们深入介绍了如何创建和调用函数。函数可以调用其他函数,但可能让你感到惊讶的是,函数还可以调用自己。

递归是怎么定义的呢?

递归:参见"递归”

这种解释挺蠢的,我们再来看一段代码,它是一个函数调用自身的实例

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开始往后乘
  • 寻找规律乘法运算中的后一个乘数比前一个乘数大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!就统统都知道了。

这种思路就是递归。相比于递推的"由小及大",递归更像是"由大及小"。我们再来看一看这道题,由大及小的分析一下

  • n的阶乘是n-1的阶乘乘以n
  • 1的阶乘是1

只要有了最小问题的解(基线条件),和通解(递归条件)那么我们就可以动手写函数了

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为整数

递推的思路:

  • x的0次方为1
  • x的n次方是n个x相乘

代码:

def get_res(x,n):
  result = 1
  for i in range(0,n):
    result *= x
  return result

换做递归呢?思路同样也简单:

  • 任非零整数的0次幂为1
  • xn 是x * xn-1的积

代码:

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次。二分查找的效率非常高。

总结

  1. 使用递归函数的优点是逻辑简单清晰,缺点是过深的调用会导致栈溢出。
  2. 递归需要寻找基线条件,总结递归条件
  3. 递推需要总结规律,寻找边界值
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值