使用Python语言理解递归
递归
一个函数在执行过程中一次或多次调用其本身便是递归,就像是俄罗斯套娃一样,一个娃娃里包含另一个娃娃。
递归其实是程序设计语言学习过程中很快就会接触到的东西,但有关递归的理解可能还会有一些遗漏,下面对此方面进行更加深入的理解
递归的分类
这里根据递归调用的数量分为线性递归、二路递归与多重递归
线性递归
如果一个递归调用最多开始一个其他递归调用,我们称之为线性递归。
例如:
def binary_search(data, target, low, high):
"""
二分查找,对有序列表进行查找,如果找到则返回True,否则返回False
"""
if low > high:
return False
else:
mid = (low + high) // 2
if target == data[mid]:
return True
elif target < data[mid]:
return binary_search(data, target, low, mid - 1)
else:
return binary_search(data, target, mid + 1, high)
虽然在代码中有两个binary_search,但他们是不同条件执行的,每次只能执行一次,所以是线性递归。
二路递归
如果一个递归调用可以开始两个其他递归调用,我们称之为二路递归
例如:
def binary_sum(S, start, stop):
"""
二路递归计算一个序列的和,例如S[0:5],就像切片的范围一样
"""
if start >= stop:
return 0
elif start == stop - 1:
return S[start]
else:
mid = (start + stop) // 2
return binary_sum(S, start, mid) + binary_sum(S, mid, stop)
这个递归每次执行都会调用两次该函数,所以说是二路递归,每次递归后,范围缩小一半,所以该递归的深度是1+logn
多重递归
如果一个递归调用可以开始三个或者更多其他递归调用,我们称之为多重递归
例如:
import os
def disk_usage(path):
"""
计算一个文件系统的磁盘使用情况,
"""
total = os.path.getsize(path)
if os.path.isdir(path):
for filename in os.listdir(path):
childpath = os.path.join(path, filename)
total += disk_usage(childpath)
print('{0:<7}'.format(total), path)
return total
os.path.getsize为获得标识的文件或者目录使用的即时磁盘空间大小。我理解的是如果该标识的是一个文件,那么就是获得该文件的大小,如果是一个文件夹的话,那就是获得该文件夹的大小,但不包括文件夹里边的内容,就像是一个盒子中放了很多物品,但这里只计算了盒子的重量,但没有计算物品的重量,也就是计算了一个空盒子。所以这个递归函数中的递归调用次数取决于这一层文件或文件夹的数量,所以是多重递归。
递归的不足
递归的不足显然就是时间与空间的消耗,具体可以参考https://www.cnblogs.com/sfencs-hcy/p/10171457.html ,这篇文章中使用了缓存的方法减少了斐波那契数列的计算消耗,在这里我们使用另一种方式来改善那种坏的递归:
def fibonacci(n):
"""
斐波那契数列计算,返回的是一个元组
"""
if n <= 1:
return (n, 0)
else:
(a, b) = fibonacci(n - 1)
return(a + b, a)
将原来的二路递归改为了线性递归,减少了重复的计算。
python的最大递归深度
每一次递归都会有资源的消耗,每一次连续的调用都会需要额外的内存,当产生无限递归时,那就意味着资源的迅速耗尽,这明显是不合理的。python为了避免这种现象,在设计时有意的限制了递归的深度,我们可以测试一下
def limitless(n):
print('第' + str(n) + '次调用')
n += 1
return limitless(n)
limitless(1)
第988次调用
第989次调用
第990次调用
第991次调用
第992次调用
第993次调用
第994次调用
第995次调用
第996次调用
Traceback (most recent call last):
File “D:/github/Data-Structure/code/递归.py”, line 73, in
limitless(1)
File “D:/github/Data-Structure/code/递归.py”, line 70, in limitless
return limitless(n)
File “D:/github/Data-Structure/code/递归.py”, line 70, in limitless
return limitless(n)
File “D:/github/Data-Structure/code/递归.py”, line 70, in limitless
return limitless(n)
[Previous line repeated 992 more times]
File “D:/github/Data-Structure/code/递归.py”, line 68, in limitless
print(‘第’ + str(n) + ‘次调用’)
RecursionError: maximum recursion depth exceeded while calling a Python object
最终递归到996次停止了递归,也就是python的递归深度限制在了1000附近。
1000层的限制已经是足够的了,但是还是有可能限制到某些计算,所以python提供了一个修改限制的方式
import sys
def limitless(n):
print('第' + str(n) + '次调用')
n += 1
return limitless(n)
print(sys.getrecursionlimit())#1000
sys.setrecursionlimit(2000)
limitless(1)
第1994次调用
第1995次调用
第1996次调用
Traceback (most recent call last):
File “D:/github/Data-Structure/code/递归.py”, line 70, in limitless
return limitless(n)
File “D:/github/Data-Structure/code/递归.py”, line 70, in limitless
return limitless(n)
File “D:/github/Data-Structure/code/递归.py”, line 70, in limitless
return limitless(n)
[Previous line repeated 995 more times]
File “D:/github/Data-Structure/code/递归.py”, line 68, in limitless
print(‘第’ + str(n) + ‘次调用’)
RecursionError: maximum recursion depth exceeded while calling a Python object
可见把这个深度该为2000后便多了1000次调用,但这个深度显然不是设置多少就是多少,毕竟还有计算机CPU与内存的限制,比如吧深度改为10000,那么运行后
第3920次调用
第3921次调用
第3922次调用
第3923次调用Process finished with exit code -1073741571 (0xC00000FD)
到达3923次便终止了,查询-1073741571发现是递归栈溢出的问题。
尾递归
如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码。
Python解释器在对于一次函数调用中,会使用一个栈帧来保存当前调用的函数的信息,如输入参数、返回值空间、计算表达式时用到的临时存储空间、函数调用时保存的状态信息以及输出参数。因此在递归的调用中,这种未执行完的函数会一层一层的占用大量的栈帧。如果将递归的调用放到函数执行的最后一步,那么执行完这步,该次函数的栈帧就会释放,调用函数的新栈帧就会替换掉之前的栈帧,所以无论调用的深度有多少次,都只会占用一个栈帧,那也就不会发生栈溢出的问题。这就是尾递归。
所以根据需要,尾递归必须是线性递归,并且递归调用的返回值必须立即返回。
拿一个阶乘递归函数举例
def factorial(n):
"""
阶乘递归函数
"""
if n == 0:
return 1
else:
return n * factorial(n - 1)
上边这个是一个普通的递归,下面把他改成尾递归的形式
def facttail(n, res):
"""
阶乘尾递归,res初始为1
"""
if n < 0:
return 0
elif n == 0:
return 1
elif n == 1:
return res
else:
return facttail(n - 1, n *res)
这个函数比之前那个还多了个res,第一种每次调用完要乘n,这里的res就起了相同的作用,由于尾递归每一层的栈帧要释放,所以通过res来作为相乘的过程。我个人认为尾递归的难度就在于参数的设计,因为它的前提条件就是调用后什么也不再执行了,所以要作为传递的东西就得提前通过参数设计传递,总之要想设计一个尾递归的算法还是需要好好思考一下的。
参考《数据结构与算法Python语言实现》
使用Python语言理解递归的更多相关文章
- 求斐波那契数的python语言实现---递归和迭代
迭代实现如下: def fab(n): n1 = 1 n2 = 1 if n<1: print("输入有误!") return -1 while (n-2)>0: n3 ...
- 010 深入理解Python语言
目录 一.概述 二.计算机技术的演进 2.1 计算机技术的演进过程 三.编程语言的多样初心 3.1 编程语言有哪些? 3.2 不同编程语言的初心和适用对象 3.3 2018年以后的计算环境- 四.Py ...
- 深入理解python语言
2008年,安卓操作系统诞生:PC时代向移动时代转换 互联网,视窗 2017/5/27柯洁最终0:3AlphaGo 计算机技术的演进过程 不同编程语言的设计初心和适用对象 C语言核心解决的是性能问题, ...
- 第三章 深入理解python语言
计算机技术的演进过程 1946-1981年 计算机系统结构时代(35年) 解决计算机能力的问题 1981-2008年 网络和视窗时代(27年) 解决交互问题 2008-2016年 复杂信息系统时代(8 ...
- 如何系统地自学一门Python 语言(转)
转自:http://www.phpxs.com/post/4521 零基础情况下,学一门语言充实下自己,Python,简洁.优美.容易使用,是一个很好的选择.那么如何系统地自学Python呢? 有的人 ...
- 动态语言的灵活性是把双刃剑 -- 以Python语言为例
本文有些零碎,总题来说,包括两个问题:(1)可变对象(最常见的是list dict)被意外修改的问题,(2)对参数(parameter)的检查问题.这两个问题,本质都是因为动态语言(动态类型语言)的特 ...
- python语言的基本要素
python语言的基本要素 一.基本的数据类型: 数字类型:整型.浮点型.复数 序列类型:字符串.时间日期 容器类型:列表.元祖.字典(散列表).集合 组合数据类型(容器类型所装载的数据构成数据集合) ...
- 动态语言的灵活性是把双刃剑 -- 以 Python 语言为例
本文有些零碎,总题来说,包括两个问题:(1)可变对象(最常见的是list dict)被意外修改的问题,(2)对参数(parameter)的检查问题.这两个问题,本质都是因为动态语言(动态类型语言)的特 ...
- 小白专场-是否同一颗二叉搜索树-python语言实现
目录 一.二叉搜索树的相同判断 二.问题引入 三.举例分析 四.方法探讨 4.1 中序遍历 4.2 层序遍历 4.3 先序遍历 4.4 后序遍历 五.总结 六.代码实现 一.二叉搜索树的相同判断 二叉 ...
随机推荐
- SQL Server性能优化(11)非聚集索引的覆盖索引存储结构
一,非聚集索引的include 非聚集索引的Include属性可以让非聚集索引包含其他列.如 CREATE NONCLUSTERED INDEX [NonIxUser] ON [dbo].[Users ...
- 《Kubernetes权威指南》——网络原理
1 Kubernetes网络模型 基本原则:每个Pod都拥有一个独立IP,而且假定所有Pod都在一个可以直接连通的.扁平的网络空间中. 基于基本原则,用户不需要额外考虑如何建立Pod之间的连接,也不需 ...
- 【PyTorch深度学习60分钟快速入门 】Part1:PyTorch是什么?
0x00 PyTorch是什么? PyTorch是一个基于Python的科学计算工具包,它主要面向两种场景: 用于替代NumPy,可以使用GPU的计算力 一种深度学习研究平台,可以提供最大的灵活性 ...
- 百度编辑器 UEditor setContent()
给编辑器赋值的代码: var ue = UE.getEditor('content'); ue.ready(function (){ ue.setContent(data.data.cont ...
- linux下修改时间和时区
一.修改linux的时间在root用户下,使用date指令:date -s1.只修改日期,不修改时间,输入: date -s -- 2.只修改时间,输入: date -s :: 3.同时修改日期时间, ...
- 人生第一次成功的sql注入
看了一些学习视频,按着大神们的教程,搜索inurl:asp?id=,结果一个可以注入的站点都找不到,绝望. 不放弃,又找啊找,找啊找,终于找到了一个! 啦啦啦,注入点!ヾ(o◕∀◕)ノヾ!! 再来查数 ...
- Java设计模式学习记录-责任链模式
前言 已经把五个创建型设计模式和七个结构型设计模式介绍完了,从这篇开始要介绍行为型设计模式了,第一个要介绍的行为型设计模式就是责任链模式(又称职责链模式). 责任链模式 概念介绍 责任链模式是为了避免 ...
- Python 3 进阶 —— 使用 PyMySQL 操作 MySQL
PyMySQL 是一个纯 Python 实现的 MySQL 客户端操作库,支持事务.存储过程.批量执行等. PyMySQL 遵循 Python 数据库 API v2.0 规范,并包含了 pure-Py ...
- SQL Server新建登录名,实现SQL Server身份验证模式
一.如果是默认安装,没有选择SQL Server和Windows身份验证模式,需要重新设置. 1.右击服务器,属性,常规项中点选“SQL Server和Windows身份验证模式” 2.点击“确定”按 ...
- 《深入理解Java虚拟机》(一)Java虚拟机发展史
Java虚拟机发展史 1.Sun Classic/Exact VM 1.Sun Classic:世界第一款商用Java虚拟机. 2.Exact VM:准确式GC:虚拟机可以知道内存中的某个位置的数据具 ...