浅谈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 Paramiko模块的安装与使用详解
Nov 18 Python
Python中创建字典的几种方法总结(推荐)
Apr 27 Python
Python模拟随机游走图形效果示例
Feb 06 Python
浅谈Pandas 排序之后索引的问题
Jun 07 Python
Django使用redis缓存服务器的实现代码示例
Apr 28 Python
Python批量生成幻影坦克图片实例代码
Jun 04 Python
Python使用scrapy爬取阳光热线问政平台过程解析
Aug 14 Python
python3实现弹弹球小游戏
Nov 25 Python
Python绘图实现显示中文
Dec 04 Python
Python嵌套函数,作用域与偏函数用法实例分析
Dec 26 Python
django为Form生成的label标签添加class方式
May 20 Python
python如何爬取网页中的文字
Jul 28 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 操作文件的一些FAQ总结
2009/02/12 PHP
php xml实例 留言本
2009/03/20 PHP
使用PHP Socket写的POP3类
2013/10/30 PHP
PHP中的随机性 你觉得自己幸运吗?
2016/01/22 PHP
PHP类的声明与实例化及构造方法与析构方法详解
2016/01/26 PHP
PHP用mysql_insert_id()函数获得刚插入数据或当前发布文章的ID
2016/11/25 PHP
PHP数组相加操作及与array_merge的区别浅析
2016/11/26 PHP
ThinkPHP3.2框架自定义配置和加载用法示例
2018/06/14 PHP
javascript之大字符串的连接的StringBuffer 类
2007/05/08 Javascript
javascript转换字符串为dom对象(字符串动态创建dom)
2010/05/10 Javascript
JS弹出窗口代码大全(详细整理)
2012/12/21 Javascript
js showModalDialog参数的使用详解
2014/01/07 Javascript
js获取下拉列表框中的value和text的值示例代码
2014/01/11 Javascript
js的Boolean对象初始值示例
2014/03/04 Javascript
js实现仿网易点击弹出提示同时背景变暗效果
2015/08/13 Javascript
js为什么不能正确处理小数运算?
2015/12/29 Javascript
基于BootStarp的Dailog
2016/04/28 Javascript
JQuery控制DIV的选取实现方法
2016/09/18 Javascript
jQuery实现字体颜色渐变效果的方法
2017/03/29 jQuery
老生常谈js-react组件生命周期
2017/05/02 Javascript
vue2.x 父组件监听子组件事件并传回信息的方法
2017/07/17 Javascript
vue-router路由懒加载和权限控制详解
2017/12/13 Javascript
JS中的JSON对象的定义和取值实现代码
2018/05/09 Javascript
从vue源码解析Vue.set()和this.$set()
2018/08/30 Javascript
JS实现滑动导航效果
2020/01/14 Javascript
[01:32]2014DOTA2西雅图邀请赛 CIS我们有信心进入正赛
2014/07/08 DOTA
[46:55]Ti4 冒泡赛第二轮 LGD vs C9
2014/07/14 DOTA
Python模拟百度登录实例详解
2016/01/20 Python
利用Python破解验证码实例详解
2016/12/08 Python
浅谈Python Opencv中gamma变换的使用详解
2018/04/02 Python
django 实现将本地图片存入数据库,并能显示在web上的示例
2019/08/07 Python
小学信息技术教学反思
2014/02/10 职场文书
国家税务局干部作风整顿整改措施
2014/09/18 职场文书
Oracle笔记
2021/04/05 Oracle
vue el-table实现递归嵌套的示例代码
2022/08/14 Vue.js
mysql通过group by分组取最大时间对应数据的两种有效方法
2022/09/23 MySQL