浅谈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 wxPython库Core组件BoxSizer用法示例
Sep 03 Python
使用Django2快速开发Web项目的详细步骤
Jan 06 Python
python3+PyQt5 数据库编程--增删改实例
Jun 17 Python
python修改字典键(key)的方法
Aug 05 Python
Python中调用其他程序的方式详解
Aug 06 Python
python3.7 openpyxl 删除指定一列或者一行的代码
Oct 08 Python
Python2与Python3的区别点整理
Dec 12 Python
Django ORM实现按天获取数据去重求和例子
May 18 Python
解决python运行启动报错问题
Jun 01 Python
linux mint中搜狗输入法导致pycharm卡死的问题
Oct 28 Python
Django如何继承AbstractUser扩展字段
Nov 27 Python
关于python中模块和重载的问题
Nov 02 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 网页播放器用来播放在线视频的代码(自动判断并选择视频文件类型)
2010/06/03 PHP
基于php-fpm的配置详解
2013/06/03 PHP
详谈symfony window下的安装 安装时候出现的问题以及解决方法
2017/09/28 PHP
XmlUtils JS操作XML工具类
2009/10/01 Javascript
被jQuery折腾得半死,揭秘为何jQuery为何在IE/Firefox下均无法使用
2010/01/22 Javascript
两种方法基于jQuery实现IE浏览器兼容placeholder效果
2014/10/14 Javascript
jQuery中queue()方法用法实例
2014/12/29 Javascript
移动端js图片查看器
2016/11/17 Javascript
socket.io学习教程之基础介绍(一)
2017/04/29 Javascript
设置cookie指定时间失效(实例代码)
2017/05/28 Javascript
Node.js 中使用 async 函数的方法
2017/11/20 Javascript
详解如何配置vue-cli3.0的vue.config.js
2018/08/23 Javascript
详解JavaScript原生封装ajax请求和Jquery中的ajax请求
2019/02/14 jQuery
axios+Vue实现上传文件显示进度功能
2019/04/14 Javascript
ES6知识点整理之对象解构赋值应用示例
2019/04/17 Javascript
jquery操作checkbox的常用方法总结【附测试源码下载】
2019/06/10 jQuery
JavaScrip如果基于url实现图片下载
2020/07/03 Javascript
Vue v-for中的 input 或 select的值发生改变时触发事件操作
2020/08/31 Javascript
react+antd 递归实现树状目录操作
2020/11/02 Javascript
[45:32]Liquid vs LGD 2018国际邀请赛淘汰赛BO3 第二场 8.23
2018/08/24 DOTA
python生成指定尺寸缩略图的示例
2014/05/07 Python
python3操作mysql数据库的方法
2017/06/23 Python
简单谈谈Python中的json与pickle
2017/07/19 Python
python简单实现操作Mysql数据库
2018/01/29 Python
python使用itchat实现手机控制电脑
2018/02/22 Python
Python matplotlib学习笔记之坐标轴范围
2019/06/28 Python
python3.7 使用pymssql往sqlserver插入数据的方法
2019/07/08 Python
TensorFLow 数学运算的示例代码
2020/04/21 Python
分享PyCharm最新激活码(真永久激活方法)不用每月找安装参数或最新激活码了
2020/12/27 Python
html5标记文字_动力节点Java学院整理
2017/07/11 HTML / CSS
全球游戏Keys和卡片市场:GamesDeal
2018/03/28 全球购物
加拿大专业美发产品购物网站:Chatters
2021/02/28 全球购物
外贸公司实习自我鉴定
2013/09/24 职场文书
个人简历中自我评价
2014/02/11 职场文书
借款担保书范文
2014/05/13 职场文书
酒店厨房管理制度
2015/08/06 职场文书