浅谈Python 递归算法指归


Posted in Python onAugust 22, 2019

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为正整数集)

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

def factorial(n):
 if n == 0: # 递归出口
 return 1
 return n*factorial(n-1) # 向递归出口方向靠近的自身调用

def fibonacci(n):
 if n < 2: # 递归出口
 return 1
 return fibonacci(n-1) + fibonacci(n-2) # 向递归出口方向靠近的自身调用

这两个函数的结构都非常简单,递归出口和自身调用清晰明了,但二者有一个显著的区别:阶乘函数中,只用一次自身调用,而斐波那契函数则有两次自身调用。

阶乘递归函数每一层的递归对自身的调用只有一次,因此每一层次上至多只有一个实例,且它们构成一个线性的次序关系。此类递归模式称作“线性递归”,这是递归最基本形式。非线性递归(比如斐波那契递归函数)在每一层上都会产生两个实例,时间复杂度为浅谈Python 递归算法指归,极易导致堆栈溢出。

其实,用循环的方法同样可以简洁地写出上面两个函数。的确,很多情况下,递归能够解决的问题,循环也可以做到。但是,更多的情况下,循环是无法取代递归的。因此,深入研究递归理论是非常有必要的。

3. 尾递归

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

def factorial_A(n):
 if n == 0: # 递归出口
 return 1
 return n*factorial_A(n-1) # 向递归出口方向靠近的自身调用

def factorial_B(n, k=1):
 if n == 0: # 递归出口
 return k
 k *= n
 n -= 1
 return factorial_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. 单向递归

前文中两个递归函数,不管是阶乘还是斐波那契数列,递归总是向着递归出口方向进行,没有分支,没有反复,这样的递归,我们称之为单向递归。在文件递归遍历等应用场合,搜索完一个文件夹,通常要返回至父级目录,继续搜索其他兄弟文件夹,这个过程就不是单向的,而是有分叉的、带回溯的。通常复杂递归都不是单向的,算法设计起来就比较困难。

import os

def ergodic(folder):
 for root, dirs, files in os.walk(folder):
 for dir_name in dirs:
  print(os.path.join(root, dir_name))
 for file_name in files:
  print(os.path.join(root, file_name))

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

import os

def ergodic(folder):
 for item in os.listdir(folder):
 obj = os.path.join(folder, item)
 print(obj)
 if os.path.isdir(obj):
  ergodic(obj)

5. 深度优先与广度优先

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

import os

def ergodic_DFS(folder):
 """基于深度优先的文件遍历"""
 
 dirs, files = list(), list()
 for item in os.listdir(folder):
 if os.path.isdir(os.path.join(folder, item)):
  dirs.append(item)
 else:
  files.append(item)
 
 for dir_name in dirs:
 ergodic(os.path.join(folder, dir_name))
 for file_name in files
 print(os.path.join(folder, file_name))

def ergodic_BFS(folder):
 """基于广度优先的文件遍历"""
 
 dirs, files = list(), list()
 for item in os.listdir(folder):
 if os.path.isdir(os.path.join(folder, item)):
  dirs.append(item)
 else:
  files.append(item)
 
 for file_name in files
 print(os.path.join(folder, file_name))
 for dir_name in dirs:
 ergodic(os.path.join(folder, dir_name))

 以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
从零学Python之引用和类属性的初步理解
May 15 Python
Python中利用aiohttp制作异步爬虫及简单应用
Nov 29 Python
Python面向对象程序设计多继承和多态用法示例
Apr 08 Python
python实现猜拳小游戏
Apr 05 Python
Python使用matplotlib实现交换式图形显示功能示例
Sep 06 Python
python将邻接矩阵输出成图的实现
Nov 21 Python
Python 支持向量机分类器的实现
Jan 15 Python
tensorflow实现从.ckpt文件中读取任意变量
May 26 Python
python统计mysql数据量变化并调用接口告警的示例代码
Sep 21 Python
基于Python中Remove函数的用法讨论
Dec 11 Python
用Python仅20行代码编写一个简单的端口扫描器
Apr 08 Python
Python使用pandas导入csv文件内容的示例代码
Dec 24 Python
python求加权平均值的实例(附纯python写法)
Aug 22 #Python
python求平均数、方差、中位数的例子
Aug 22 #Python
python2和python3实现在图片上加汉字的方法
Aug 22 #Python
Python使用微信itchat接口实现查看自己微信的信息功能详解
Aug 22 #Python
简单了解python 生成器 列表推导式 生成器表达式
Aug 22 #Python
Python实现的微信红包提醒功能示例
Aug 22 #Python
Python PIL图片添加字体的例子
Aug 22 #Python
You might like
PHP学习之数组的定义和填充
2011/04/17 PHP
php中将汉字转换成拼音的函数代码
2012/09/08 PHP
PHP操作MongoDB GridFS 存储文件的详解
2013/06/20 PHP
thinkphp循环结构用法实例
2014/11/24 PHP
实例讲解PHP设计模式编程中的简单工厂模式
2016/02/29 PHP
WordPress的文章自动添加关键词及关键词的SEO优化
2016/03/01 PHP
如何让PHP编码更加好看利于阅读
2019/05/12 PHP
getElementById在任意一款浏览器中都可以用吗的疑问回复
2007/05/13 Javascript
jQuery中.live()方法的用法深入解析
2013/12/30 Javascript
javascript:window.open弹出窗口的位置问题
2014/03/18 Javascript
轻松使用jQuery双向select控件Bootstrap Dual Listbox
2015/12/13 Javascript
关于vue-router路径计算问题
2017/05/10 Javascript
[45:25]完美世界DOTA2联赛循环赛 PXG vs IO 第一场 11.06
2020/11/09 DOTA
详解Python编程中time模块的使用
2015/11/20 Python
浅谈python类属性的访问、设置和删除方法
2016/07/25 Python
Python自定义函数定义,参数,调用代码解析
2017/12/27 Python
python2.7实现邮件发送功能
2018/12/12 Python
75条笑死人的知乎神回复,用60行代码就爬完了
2019/05/06 Python
Python3.5 win10环境下导入kera/tensorflow报错的解决方法
2019/12/19 Python
PyTorch中 tensor.detach() 和 tensor.data 的区别详解
2020/01/06 Python
如何用用Python将地址标记在地图上
2021/02/07 Python
官方授权图形T恤和服装:Fifth Sun
2019/06/12 全球购物
通用自荐信范文
2014/03/14 职场文书
《地震中的父与子》教学反思
2014/04/10 职场文书
2014年征兵标语
2014/06/20 职场文书
交通事故委托书范本(2篇)
2014/09/21 职场文书
生产工厂门卫岗位职责
2014/09/26 职场文书
2014年学生会工作总结范文
2014/11/07 职场文书
工程部岗位职责
2015/02/10 职场文书
美丽心灵观后感
2015/06/01 职场文书
小数乘法教学反思
2016/02/22 职场文书
css3 实现文字闪烁效果的三种方式示例代码
2021/04/25 HTML / CSS
pytorch finetuning 自己的图片进行训练操作
2021/06/05 Python
十大最帅动漫男主 碓冰拓海上榜,第一是《灌篮高手》男主角
2022/03/18 日漫
分享Python异步爬取知乎热榜
2022/04/12 Python
Linux中如何安装并部署Redis
2022/04/18 Servers