理解递归的本质:递归与栈
理解递归的本质:递归与栈
2018年04月11日 09:34:08 bobbypollo 阅读数:3464
版权声明:本BLOG上原创文章未经本人许可,不得用于商业用途及传统媒体。网络媒体转载请注明出处,否则属于侵权行为。 https://blog.csdn.net/bobbypollo/article/details/79891556
递归的基本思想
所谓递归,就是有去有回。
递归的基本思想,是把规模较大的一个问题,分解成规模较小的多个子问题去解决,而每一个子问题又可以继续拆分成多个更小的子问题。
最重要的一点就是假设子问题已经解决了,现在要基于已经解决的子问题来解决当前问题;或者说,必须先解决子问题,再基于子问题来解决当前问题。
或者可以这么理解:递归解决的是有依赖顺序关系的多个问题。
我们假设一个抽象问题有两个时间点要素:开始处理,结束处理。
那么递归处理的顺序就是,先开始处理的问题,最后才能结束处理。
假设如下问题的依赖关系:
【A】----依赖---->【B】----依赖---->【C】
我们的终极目的是要解决问题A,
那么三个问题的处理顺序如下:
开始处理问题A;
由于A依赖B,因此开始处理问题B;
由于B依赖C,开始处理问题C;
结束处理问题C;
结束处理问题B;
结束处理问题A。
从函数调用看广义递归
对于软件来说,函数的调用关系就是一个广义递归的过程,如下,
func_A()
{
func_B();
}
func_B()
{
func_C();
}
func_C()
{
/////
}
调用函数A;
调用函数B;
调用函数C;
函数C返回;
函数B返回;
函数A返回;
狭义递归函数
有一种特例,就是处理问题A/B/C的方法是一样的,这就是产生了狭义的“递归函数“,即函数内又调用函数自身。
从上述分析看,递归对问题的处理顺序,是遵循了先入后出(也就是先开始的问题最后结束)的规律。
先入后出?栈!
没错,广义递归问题的处理,需要用栈来解决。经典的例子就是函数调用,就是依靠栈来实现的。
递归函数的非递归化
现在再来深入分析一下狭义的递归函数(也就是函数调用自身)。
我们知道递归函数存在的最大问题是,当递归次数足够大时,会导致函数栈溢出而死机,函数栈的大小一般是一个固定值,对于linux来说一般默认是8M。
因此,编程老司机会教导我们,不得用递归函数!但递归函数的代码实现实在是简洁啊,不让用?臣妾做不到啊!
那么问题来了,所有递归函数都能非递归化吗?答案是肯定的。
本质上讲,对于同一个问题,如果必然要用广义递归的方案来处理,那么狭义递归函数只不过是其中的一种实现方式,如果放弃狭义递归函数的话,我们不得不借助一个额外的数据结构:栈。
如此看来,无论如何都要用到栈,只不过要么让编译器来维护一个栈(函数栈),要么让程序狗来维护一个栈(数据栈)。
这两个栈的区别如下:
函数栈 |
数据栈 |
|
位置 |
进程的stack区 |
进程的heap区 |
大小限制 |
小(8M?) |
能分配到很大 |
每个栈“元素”所需空间 |
比较大,因为要存储函数上下文 |
可以设计到很小,比如只存储一个指针 |
栈开销 |
大 |
可以做的很小 |
代码简易程度/可维护性 |
简洁易读 |
相对更复杂 |
注:函数栈开销是一个绝对值,但也算是一个“相对“概念,一个非量化的理性分析是,内部逻辑越简单的函数,栈开销的影响越大,因为函数的出入栈指令占整个函数体指令的比重较大。
很多情况下,代码的易维护性是一个比性能开销更加重要的因素,因此,只要实际应用中不会造成函数栈溢出,我个人是更建议采用递归函数法的。
举例说明:二叉树的非递归遍历
非递归算法的递归化
既然递归算法可以用数据栈来进行非递归化,那么借助数据栈而实现的非递归算法,理论上也可以被递归化。也就是说,两者是可逆的,桥梁就是栈。
- 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 数组属性和方法
- 大数据量下的集合过滤—Bloom Filter
- 实时数仓链路分享:kafka =>SparkStreaming=>kudu集成kerberos
- rocketmq broker启动报错,找不到或无法加载主类
- 视频监控联网RTSP平台EasyNVR用户管理权限与实际权限不匹配,该如何排查?
- 2020CHINC,来赴一场“共建智慧医院”的约会
- 《闲扯Redis十》Redis 跳跃表的结构实现
- 图数据库HugeGraph源码解读 (1) —— 入门介绍
- String及StringTable(一):String源码解读
- 四、原型模式与建造者模式详解
- LeetCode 23. 移动零
- LeetCode 11. 盛最多水的容器
- 29.MyBatis体系结构与工作原理
- 五、代理模式详解
- 六、门面模式与装饰器模式详解
- 七、享元模式与门面模式详解