当前位置: 首页 > 图灵资讯 > 行业资讯> 一文了解Python中的递归

一文了解Python中的递归

发布时间:2025-04-06 15:46:40

1. 递归概述

递归( recursion)这是一种编程技能,在某些情况下,甚至是不可替代的技能。递归可以极大地简化代码,看起来非常简单,但递归设计非常抽象,不容易掌握。通常,我们从上到下思考, 递归是自下而上的问题——这就是递归看起来不够直观的原因。那么,递归到底是什么呢?让我们从生活中找到栗子。

我们都有在黑暗的放映厅找座位的经验:问前排的朋友坐在第一排,加上一个,是他们目前的位置。如果前排的朋友不知道他是什么排,他可以用同样的方法得到他的排名,然后告诉你。如果前排的朋友不知道他是什么排,他就会这样做。这样的推导,不会系统地继续下去,因为当被问及第一排时,坐在第一排的朋友会直接给出答案。这就是递归算法在生活中的应用实例。

至于递归,不太严格的定义是“一个函数在运行过程中直接或间接调用自己”。如果更严格,递归函数必须满足以下两个条件:

(1)至少有一个明确的递归结束条件,我们称之为递归出口,有些人喜欢称之为递归基。

(2)直接或间接地调用自己,接近递归出口方向(也称递归调用)。

虽然递归晦涩难懂,但也有规律可循。只有掌握了基本的递归理论,才能将其应用到复杂的算法设计中。

2. 线性递归

让我们从最经典的两种递归算法开始-阶乘(factorial)以及斐波那契数列(Fibonacci sequence)。几乎所有讨论递归算法的话题都是从它们开始的。阶级的概念相对简单。唯一需要解释的是,0的阶级是1而不是0。为此,我特别问了我的女儿,她是一名数学专业的学生。斐波那契数列,又称黄金分割数列,是指这样一个数列:1、1、2、3、5、8、13、21、34、...斐波纳契数列在数学上是这样定义的:

F(0)=1,F(1)=1,F(n)=F(n-1)+F(n-2)(n>=2,n∈N,N为正整数集)

阶乘和斐波那契数列的递归算法如下:

deffactorial(n):
ifn==0:#递归出口
return1
returnn*factorial(n-1)#接近递归出口方向的自我调用
deffibonacci(n):
ifn<2:#递归出口
return1
returnfibonacci(n-1)+fibonacci(n-2)#接近递归出口方向的自我调用

这两个函数的结构都很简单,递归出口和自身调用都很清楚,但是两者有一个明显的区别:阶乘函数只调用一次,而斐波那契函数调用两次。

阶级递归函数每层的递归只调用一次,所以每层最多只有一个例子,它们构成一个线性顺序关系。这种递归模式被称为“线性递归”,这是递归最基本的形式。非线性递归(如斐波那契递归函数)在每层产生两个例子,时间复杂性为e9b2703e64583d96c5ed09900f555b7.png

,容易导致堆栈溢出。

事实上,上述两个函数也可以通过循环简单地写出来。事实上,在许多情况下,递归可以解决问题,循环也可以实现。然而,在更多的情况下,循环并不能取代递归。因此,有必要深入研究递归理论。

3. 尾递归

接下来,我们将以上阶乘递归函数进行改造,仍然以递归的方式实现。为了便于比较,我们将两种算法放在一起。

deffactorial_A(n):
ifn==0:#递归出口
return1
returnn*factorial_A(n-1)#接近递归出口方向的自我调用
deffactorial_B(n,k=1):
ifn==0:#递归出口
returnk
k*=n
n-=1
returnfactorial_B(n,k)#接近递归出口方向的自我调用

比较 factorial_A() 和 factorial_B() 写作,会发现很有趣的问题。factorial_A() 自我调用是表达式的一部分,这意味着自我调用不是函数的最后一步,而是需要在获得自我调用结果后再次乘法;factorial_B() 函数的最后一步是自我调用。就像 factorial_B() 函数是这样的,当自我调用是整个函数体中最终执行的句子,其返回值不是表达式的一部分时,这种递归调用是尾递归(Tail Recursion)。尾递归函数的特点是在回归过程中不需要任何操作,因为大多数现代编译器会利用这一特点自动生成优化代码。

分别使用 factorial_A() 和 factorial_B() 计算5的阶乘性,如下图所示的计算过程,清楚地显示了尾递归的优点:不需要花费大量的堆栈空间来保存上次递归的参数、局部变量等,这是因为上次递归操作后,计算了以前的数据,并将其传递给当前的递归函数,因此上次递归的局部变量和参数将被删除并释放空间,以免造成堆栈溢出。

factorial_A(5)
5*factorial_A(4)
5*4*factorial_A(3)
5*4*3*factorial_A(2)
5*4*3*2*factorial_A(1)
5*4*3*2*1*factorial_A(0)
5*4*3*2*1
5*4*3*2
5*4*6
5*24
120
factorial_B(5,k=1)
factorial_B(4,k=5)
factorial_B(3,k=20)
factorial_B(2,k=60)
factorial_B(1,k=120)
factorial_B(0,k=120)
120

虽然尾递归具有低消耗、高效率的优点,但这种递归通常可以转化为循环语句。

4. 单向递归

在上述两个递归函数中,无论是阶级还是斐波那契数列,递归总是朝着递归出口的方向进行,没有分支,没有重复,这种递归,我们称之为单向递归。在文件递归遍历和其他应用程序中,搜索文件夹后,通常返回到父级目录,继续搜索其他兄弟文件夹。这个过程不是单向的,而是分叉的和可追溯的。通常,复杂的递归不是单向的,算法很难设计。

importos
defergodic(folder):
forroot,dirs,filesinos.walk(folder):
fordir_nameindirs:
print(os.path.join(root,dir_name))
forfile_nameinfiles:
print(os.path.join(root,file_name))

以上是借助 os 模块的 walk() 实现基于循环的文件遍历法。虽然是循环结构,但如果不熟悉 walk() 这个函数看起来还是很不直观的。我更喜欢以下递归遍历方法。

importos
defergodic(folder):
foriteminos.listdir(folder):
obj=os.path.join(folder,item)
print(obj)
ifos.path.isdir(obj):
ergodic(obj)

5. 深度优先,广度优先

通常有两种策略:深度优先搜索 DFS(depth-first search) 优先搜索BFS和广度(breadth-first search) 。顾名思义,深度优先是对同级文件夹中的子文件夹进行优先处理,递归向深度发展;广度优先是优先处理同级文件夹中的文件,递归向水平方向发展。

importos
defergodic_DFS(folder):
"""基于深度优先级的文件遍历"""
dirs,files=list(),list()
foriteminos.listdir(folder):
ifos.path.isdir(os.path.join(folder,item)):
dirs.append(item)
else:
files.append(item)
fordir_nameindirs:
ergodic(os.path.join(folder,dir_name))
forfile_nameinfiles
print(os.path.join(folder,file_name))
defergodic_BFS(folder):
"""基于广度优先级的文件遍历"""
dirs,files=list(),list()
foriteminos.listdir(folder):
ifos.path.isdir(os.path.join(folder,item)):
dirs.append(item)
else:
files.append(item)
forfile_nameinfiles
print(os.path.join(folder,file_name))
fordir_nameindirs:
ergodic(os.path.join(folder,dir_name))

python学习网络,免费在线学习python平台,欢迎关注!

相关文章

python3兼容python2吗

python3兼容python2吗

2025-05-09
python3 whl怎么安装

python3 whl怎么安装

2025-05-09
python 字典怎么提取value

python 字典怎么提取value

2025-05-09
python 怎样计算字符串的长度

python 怎样计算字符串的长度

2025-05-09
python 怎么样反向输出字符串

python 怎么样反向输出字符串

2025-05-09
python 怎么判断字符串开头

python 怎么判断字符串开头

2025-05-09