【从0到1学算法】递归
今天我们将学习一种优雅的问题解决方式--递归。 对于它,通常有3个阵营:恨它的、爱它的以及恨了几年后又爱上它。你属于哪一个?
1、递归
递归就是函数自己调用自己,但写递归很容易出错而导致死循环。
循环和递归是可以相互转换,下面从一个简单例子学习递归。
现在需要编写一个倒计时函数。
效果如下:
3
2
1
循环方式代码:
def countdown(i):
# 从i开始, 到0结束(不包括0),每次循环-1
for j in range(i, 0, -1):
print(j)
如何用递归方式写呢?一开始你可能会这样写:
def countdown(i)
print(i)
# 变量-1,调用自己
countdown(i-1)
运行上面的代码,你会发现:这个函数运行起来没完没了!
如何正确编写递归?
编写递归,必须得告诉它如何停止。正因如此,递归函数都有这两部分:基线条件和递归条件。
递归条件指什么情况下调用自己,而基线条件指什么情况下停止调用自己。
我们只需要倒计时到1,所以这里它的基线条件便是:i<=1,其他情况都需要调用自己。
def countdown(i)
print(i)
if i <= 1:#<-----------基线条件
return
else:#<----------- 递归条件条件
countdown(i-1)
循环和递归的作用是相同的,但递归逻辑更清晰。递归也是有缺点的,有时候递归的性能不如循环,甚至很糟糕。
如果使用循环,程序性能可能更高;如果使用递归,程序可能更容易理解。如何选择,还得结合实际需求。在逻辑不特复杂的情况下,推荐使用循环。
2、栈
接下来介绍一个重要概念--栈。(递归中使用到了栈,往下看会有分析)
假设这里有一叠便利条,用于记录待办事项,那么就只能做两个操作:在最上面插入待办事项(压入)或在最上面移除并读取待办事项(弹出),再专业点说就是入栈和出栈。
更形象的例子:桶装薯片,当薯片做好之后,它们会依次被添加到桶里,每一片都是从最上面添加,而每次我们取的时候也是只能去最上面的那一片(当然你不能帮桶底捅穿),所以第一个放入桶的薯片只能最后一个从桶里取出。 这个桶装薯片就是栈,放薯片是入栈,取薯片是出栈。
3、调用栈
调用栈是在计算机内使用的栈,接下来我们看下计算机如何使用调用栈。
下面有一段简单代码。
def greet(name):
print ("hello," + name + "!")
greet2(name)
print ("getting ready to say bye...")
bye()
def greet2(name):
print ("how are you, " + name + "?")
def bye()
print ("ok bye!")
greet函数调用了另外两个函数greet2和bye。(这里,我们假设print不是一个函数,为了更简单了解调用栈的使用)
调用greet("maggie"),计算机首先会为该函数调用分配一块内存
然后将变量name设置为maggie,存储到这块内存中
每当函数被调用,计算机都会像这样将函数调用涉及的变量存储到内存中。接下来,程序打印"hello,maggie!",再调用greet2("maggie")。同样,分配内存后存储。
打印“how are you, maggie?”,greet2调用完成。此时,栈顶内存块出栈。
现在,栈顶的内存块为函数greet,意味着返回到了函数greet,继续执行未执行操作。首先打印“getting ready to say bye...”,再调用函数bye。
在栈顶添加了函数bye的内存块。然后,打印“ok bye!”,出栈并返回到函数greet。
此时,函数greet没有其他操作了, 执行完毕出栈。这便是计算机使用调用栈的过程。
4、递归调用栈
递归函数同样也使用调用栈。
下面我们以阶乘的递归函数为例,来看看递归函数中调用栈的使用。
ps: 栈顶内存块指出当前执行位置
def fact(n):
if n == 1:
return 1
else:
return n * fact(n-1)
调用fact(3)时,调用栈如何变化?
注意,栈中每个内存块相互独立,每个fact都有自己的x变量。
So easy 吧!?
通过分析调用栈在递归中的变化,我们可以得出这样的结论:递归很耗内存,每个函数调用都要占用一定的内存,如果栈很高,就意味着需要占用很大的内存。
当发现使用递归占用很大内存时,你有两种选择:
- 放弃递归,使用循环
- 使用尾递归
尾递归
这里也稍微提一下尾递归,尾递归的实质是开源节流,下面将阶乘的普通递归改为尾递归。
def fact(n, result):
if n == 1:
return result
else:
return fact(n - 1, n * result)
可以看到,在调用自己时,只有函数自身的调用,而没有其他运算,这样它就可以不在栈顶添加一个新的栈帧(栈的内存单位),而是更新它。
编写思路:尾递归需要多一个参数result用于存储每次运算的结果,最后输出结果;而n则更像是用于记录循环次数的变量。
缺点:尾递归是通过编译器优化的方式来实现效率的提升,但是有些语言或者有些编译器可能就不支持(比如python)。
5、总结
- 递归指自己调用自己。
- 递归函数都有两个条件:基线条件和递归条件(写递归时,首先找这2个条件)。
- 所有函数的调用都会进入调用栈。
- 循环的性能可能更高,递归则更容易理解,结合实际选择。
- 高盛成立交易部门,涉足比特币和加密货币交易
- WordPress主题开发:添加主题更新提醒功能
- WordPress主题开发:添加主题更新提醒功能
- ASP.NET2.0应用中定制安全凭证之实践篇
- Kaggle大神带你上榜单Top2%:点击预测大赛纪实(下)
- WordPress主题后台选项开发框架 Options Framework 介绍
- vc++ 在程序中运行另一个程序的方法
- 为Options Framework主题后台框架添加后台侧边栏
- ClistCtrl用法及总结(由怎样隐藏ListCtrl列表头的排序小三角形这个bug学习到的知识)
- 弹出式模态窗体选择文本控件
- zookeeper 分布式锁服务
- QT Creator 快速入门教程 读书笔记(三)
- WordPress中添加自定义评论表情包的方法(附三套表情包下载)
- 使用新类型Nullable处理数据库表中null字段
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- PyQt5 技巧篇-解决相对路径无法加载图片问题,styleSheet通过"相对"路径加载图片,python获取当前运行文件的绝对路径。
- 基于MMM搭建MySQL Replication集群高可用架构
- Python 技术篇-按任意格式灵活获取日期、时间、年月日、时分秒。日期格式化。
- 当删库时如何避免跑路
- Python 句法错误:"SyntaxError: invalid character in identifier",原因及解决方法
- Python3 多线程问题:ModuleNotFoundError: No module named 'thread',原因及解决办法。
- 文件传输和秒传
- 关于数据库的各种备份与还原姿势详解
- Python 技术篇-多线程的2种创建方法,多线程的简单用法,快速上手。
- Python 技术篇-调用浏览器访问指定网页,一行代码实现。非Selenium。
- 数据库热备份神器 - XtraBackup
- Python 技术篇-读取文件,将内容保存dict字典中。去掉字符串中的指定字符方法。dict字典的遍历。
- PyQt5 技术篇-plainTextEdit控件获得文本内容方法、设置文本内容方法。
- PyQt5 技术篇-鼠标移动控件显示提示,Qt Designer控件提示设置方法。
- PyQt5 技术篇-窗口名、窗口图标的设置方法。