递归计算过程与迭代计算过程
最近重新看SICP,写点感想。下面是关于递归与迭代计算的一些知识,SICP 1.2.1。
递归
递归是实现程序计算过程中的描述过程的基本模式之一,在讨论递归的问题前我们必须十分小心,因为递归包含两个方面的内容,一个是递归的计算过程,一个是递归过程,后者是语法上的事实而前者是概念上的计算过程,事实上在程序上我们也许是使用循环来实现的。
递归计算过程和我们常说的递归过程不是一回事。
- 递归过程:“当我们说一个过程是递归的时候,论述的是一个语法形式上的事实,说明这个过程的定义中(直接或者间接地)引用了该过程本身。”
- 递归计算过程:“在说某一计算过程具有某种模式时(例如,线性递归),我们说的是这一计算过程的进展方式, 而不是相应过程书写上的语法形式。”
一般在讨论递归的时候都喜欢用斐波那契数列来作为例子,斐波那契的算法也很简单,算法如下:
01
|
def Fib(n):
|
02
|
03
|
if (n < 1):
|
04
|
return 0
|
05
|
06
|
elif (n <= 2):
|
07
|
return 1
|
08
|
09
|
else :
|
10
|
return Fib(n-1)+Fib(n-2)
|
具体C语言的例子。
01
|
#include "stdio.h"
|
02
|
#include "math.h"
|
03
|
04
|
int factorial( int n);
|
05
|
06
|
int main( void )
|
07
|
{
|
08
|
int i, n, rs;
|
09
|
10
|
printf ( "请输入斐波那契数n:" );
|
11
|
scanf ( "%d" ,&n);
|
12
|
13
|
for (i = 1; i <=n; i++)
|
14
|
{
|
15
|
rs = factorial(i);
|
16
|
printf ( "%d " , rs);
|
17
|
}
|
18
|
19
|
return 0;
|
20
|
}
|
21
|
22
|
// 递归计算过程
|
23
|
int factorial( int n)
|
24
|
{
|
25
|
if (n <= 2)
|
26
|
{
|
27
|
return 1;
|
28
|
}
|
29
|
else
|
30
|
{
|
31
|
return factorial(n-1) + factorial(n-2);
|
32
|
}
|
33
|
}
|
程序运行:
1
|
请输入斐波那契数n:12
|
2
|
1 1 2 3 5 8 13 21 34 55 89 144
|
我们假设n=6,那么得到的计算过程就是,要计算Fib(6)就得计算Fib(5)和Fib(4),以此类推,如下图:
我们可以看到过程如同一棵倒置的树,这种方式被称之为树形递归,也被称之为线性递归。这种递归的方式非常的直白,很好理解其计算过程,一般很多人写递归都会下意识的采用这种方式。
但是缺点也是很明显的,从其计算过程可以看出,经过了很多冗余的计算,并且消耗了大量的调用堆栈,这个消耗是指数级增长的,经常有人说调用堆栈很容易在很短的递归过程就耗光了,多半就是采用了线性递归造成的。线性递归的过程可用下图描述,可以清晰的看到展开收拢的过程:
01
|
(factorial (6))
|
02
|
(6 * factorial (5))
|
03
|
(6 * (5 * factorial (4)))
|
04
|
(6 * (5 * (4 * factorial (3))))
|
05
|
(6 * (5 * (4 * (3 * factorial (2)))))
|
06
|
(6 * (5 * (4 * (3 * (2 * factorial (1))))))
|
07
|
(6 * (5 * (4 * (3 * (2 * 1)))))
|
08
|
(6 * (5 * (4 * (3 * 2))))
|
09
|
(6 * (5 * (4 * 6)))
|
10
|
(6 * (5 * 24))
|
11
|
(6 * 120)
|
12
|
720
|
迭代
与递归计算过程相对应的,是迭代计算过程。
除了这种递归方式还有另外一种实现递归的方式,同样是上面的斐波那契数作为例子,这次我们不按照斐波那契的定义入手,我们从正常产生数列的过程入手来实现,0,1,的情况很简单可以直接返回,之后的计算过程就是累加,我们在递归的过程中要保持状态,这个状态要保持三个数,也就是上两个数和迭代的步数,所以我们定义的方法为:
01
|
def Fib(n,b1=1,b2=1,c=3):
|
02
|
03
|
if n <= 2:
|
04
|
return 1
|
05
|
06
|
else :
|
07
|
if n==c:
|
08
|
return b1+b2
|
09
|
10
|
else :
|
11
|
return Fib(n,b1=b2,b2=b1+b2,c=c+1)
|
这种方法我们在每一次递归的过程中保持了上一次计算的状态,所以称之为“线性迭代过程”,也就是俗称的尾递归。由于每一步计算都保持了状态所以消除了冗余计算,所以这种方式的效率明显高于前一种,其计算过程如下:
1
|
fib(6)
|
2
|
fib 0,0,1
|
3
|
fib 0,1,2
|
4
|
fib 1,2,3
|
5
|
fib 2,3,4
|
6
|
fib 3,5,5
|
7
|
fib 5,8,6
|
这两种递归方式之间是可以转换的,凡是可以通过固定数量状态来描述中间计算过程的递归过程都可以通过线性迭代来表示。
“迭代计算过程是用固定数目的状态变量描述的计算过程,并存在着一套固定的规则,描述了计算过程从一个状态到下一状态转换时,这些变量的更新方式,还有一个(可能有的)结束检测,它描述这一计算过程应该中止的条件。”
以计算n的阶乘为例,其递归写为:
1
|
// 递归计算过程
|
2
|
function factorial(n){
|
3
|
if (n == 1) {
|
4
|
return 1;
|
5
|
}
|
6
|
return n * f(n-1);
|
7
|
}
|
同样是计算n的阶乘,还可以这样设计:
01
|
// 迭代计算过程
|
02
|
function factorial(n){
|
03
|
return factIterator(1, 1, n);
|
04
|
}
|
05
|
06
|
function factIterator(result, counter, maxCount){
|
07
|
if (counter > maxCount){
|
08
|
return result;
|
09
|
}
|
10
|
return factIterator((counter * result), counter + 1, maxCount);
|
11
|
}
|
它的执行过程为:
1
|
(factorial (6))
|
2
|
(factIterator(1, 1, 6))
|
3
|
(factIterator(1, 2, 6))
|
4
|
(factIterator(2, 3, 6))
|
5
|
(factIterator(6, 4, 6))
|
6
|
(factIterator(24, 5, 6))
|
7
|
(factIterator(120, 6, 6))
|
8
|
(factIterator(720, 7, 6))
|
虽然factIterator方法调用了它自己,但从它的执行过程里,所需要的所有的东西就是result,counter,和maxCount。所以它是迭代计算过程。这个过程在继续调用自身时不需要增加存储,这样的过程叫尾递归。
尾递归还可以用循环来代替:
1
|
function fib(n){
|
2
|
var a=0, b=1;
|
3
|
for (var i=0;i<=n;i++){
|
4
|
var temp = a+b;
|
5
|
a = b;
|
6
|
b = temp;
|
7
|
}
|
8
|
return b;
|
9
|
}
|
递归和迭代
递归计算过程更自然,更直截了当,可以帮助我们理解和设计程序。而要规划出一个迭代计算过程,则需设计出各个状态变量,找到迭代规律,并不是所有的递归计算过程都可以很容易的整理成迭代计算过程。
但递归计算过程会比迭代计算过程低效。
上面计算阶乘的递归计算过程属于线性递归,步骤数目的增长正比于输入n。也就是说,这个过程所需步骤的增长为O(n) ,空间需求的增长也为O(n) 。对于迭代的阶乘,步数还是O(n)而空间是O(1) ,也就是常数。
再来看斐波那契数列的递归与迭代的实现吧。
递归计算过程:
1
|
// 递归计算过程
|
2
|
function fib(n){
|
3
|
if (n <= 1){
|
4
|
return n;
|
5
|
}
|
6
|
return fib(n-1) + fib(n-2);
|
7
|
}
|
迭代计算过程、尾递归:
01
|
// 迭代计算过程、尾递归
|
02
|
function fib(n){
|
03
|
return fibIterator(1, 0, n);
|
04
|
}
|
05
|
06
|
function fibIterator(a, b, counter){
|
07
|
if (counter== 0){
|
08
|
return b;
|
09
|
}
|
10
|
return fibIterator((a+b), a, counter-1)
|
11
|
}
|
斐波那契数列的递归计算过程属于树形递归,画一下它的展开方式就可以看到。它的步数是以指数方式增长的,这是一种非常夸张的增长方式,规模每增加1,都将导致所用的资源按照某个常数倍增长。而迭代计算过程的步骤增长依然是O(n),线性增长,也就是规模增长一倍,所用的资源也增加一倍。
有时候说要减少递归,就是要减少递归计算过程,用更高效的方法代替。
我们也发现,其实尾递归的过程和循环基本上是等价的,我们可以将尾递归的过程很方便到用循环来代替,所以很多的语言对尾递归提供了编译级别的优化,也就是将尾递归在编译期转化成循环的代码。不过对于没有提供尾递归优化的语言来说也是很有意义的,比如python的默认调用堆栈长度是1000,如果用线性递归很快就会消耗光,但是尾递归就不会,比如尾递归的Fib函数,用Fib(1001)调用没问题的而且跑得飞快,Fib(1002)的时候才堆栈溢出。但是如果是线性递归的方式计算n=30的时候就能明显感觉到速度变慢,40以上基本就挂了。
这里我无意对比两种方式的优劣,也许线性递归性能有差距但是它的可读性非常的强,几乎就等同于公式的直接描述,所以可以根据计算规模来合理选用。
递归计算过程与迭代计算过程相关推荐
- 深入思考编译原理之 理解执行过程和编译过程
在深入编译器原理细节之前,很多学习者并没有真正从宏观上理解一个编译器都做了些什么,这其实是非常遗憾的. 从计算机执行过程角度思考,函数调用的尤为重要,面向过程的语言更是将函数调用作为最基本的要素.面向 ...
- 读《大道至简——失败的过程也是过程》有感
再次怀着热情读了大道至简的第五章--失败的过程也是过程.作者首先告诉我们: 做过程不是做工程 .为什么这么说?作者讲到,按照模型,做完过程的每一个阶段, 并不等于做工程.或者说,工程并不是这样就可以做 ...
- 【Android 异步操作】AsyncTask 异步任务 ( FutureTask 模拟 AsyncTask 执行过程 | AsyncTask 执行过程回顾 | FutureTask 分析 )
文章目录 一.FutureTask 使用流程 二.FutureTask 模拟 AsyncTask 执行过程 三.AsyncTask 执行过程回顾 四.FutureTask 分析 一.FutureTas ...
- View工作原理(三)视图大小计算过程(measure过程)
一.android中view的measure过程总概 视图大小计算的过程是从根视图measure()方法开始,接着该方法会调用根视图的onMeasure()方法,onMeasure()方法会对所包含的 ...
- 一个不错的SQL储存过程分页,储存过程+Repeater,如果只是浏览数据的话,快就一个字...
一个不错的SQL储存过程分页,储存过程+Repeater,如果只是浏览数据的话,快就一个字 CREATE PROCEDURE SelectPagedSQL ( @SQL nvarchar(512), ...
- Swift之深入解析构造过程和析构过程
一.Swift 构造过程 构造过程是为了使用某个类.结构体或枚举类型的实例而进行的准备过程,这个过程包含了为实例中的每个属性设置初始值和为其执行必要的准备和初始化任务. Swift 构造函数使用 in ...
- 计算机过程控制系统教材,过程控制系统-样章试读.PDF
过程控制系统-样章试读.PDF 高等院校教材 过程控制系统 陈夕松 汪木兰 主编 李 奇 主审 KB2 北 京 内 容 简 介 本书以过程控制系统组成和结构为线索,介绍了过程控制的基本概 念,过程控制 ...
- rhel系统启动过程_Linux系统启动过程
Linux启动过程 前言: Linux是一种自由和开放源代码的类UNIX操作系统.该操作系统的内核由林纳斯·托瓦兹在1991年10月5日首次发布.在加上用户空间的应用程序之后,成为Linux操作系统. ...
- 编写一个USB接口程序,模拟计算机启动过程和关闭过程启动过程中要加载鼠标、键盘、麦克风等USB设备,具体要求如下: (1)定义一个接口USB,包含两个抽象方法turnOn()he turnOff(),
一.好物推荐 给大家推荐三款蓝牙耳机,下面的链接可以直接购买: 1.https://item.taobao.com/item.htm?ft=t&id=643733003968 2.https: ...
最新文章
- 据说,程序员已成为女生年度最喜欢男友职业Top3?
- LeetCode 705. Design HashSet (设计哈希集合)
- 菜鸟教程python正则表达式_python 正则表达式详解
- groupByKey、reduceByKey区别(转)
- 一步一步SharePoint 2007之二十一:解决实现注册用户后,自动具备访问网站的权限的问题(3)——创建用户...
- 适合海报设计的最佳字体
- Windows下,Unicode、UTF8,GBK(GB2312)互转
- 酷狗音乐的临时缓存文件转换为MP3文件,java源码
- 基于Arduino控制伺服电机(舵机)
- 深度复数网络 Deep Complex Networks
- 如何将某一文件添加到信任列表?
- 2019上半年软件设计师 下午真题-简答题及免费答案(一)
- proteus7.7+Keil2仿真80C51控制流水灯
- vue组件孙子调用爷爷的方法
- ❤️熬夜爆肝十万字❤️Java最简单最全入门基础知识(一)(小白必备--推荐小白收藏)❤️
- 再谈对“重构”的学习
- 安卓虚拟键盘_0202年了,苹果IOS和安卓系统的差距还大吗?
- Linux中的进程、线程和文件描述符
- 海思平台OSD的实现(1)
- 面向对象之关键字 this