漫谈递归和迭代
递归(recursion)在计算机科学中是指一种通过重复将问题分解为同类问题的子问题而解决问题的方法。可以极大地减少代码量。递归的能力在于用有限的语句来定义对象的无限集合。递归式方法可以被用于解决很多计算机科学问题,因此它是计算机科学中十分重要的一个概念。绝大多数编程语言支持函数的自调用,在这些语言中函数可以通过调用自身来进行递归。计算理论可以证明递归可以完全取代循环,因此在很多函数编程语言中习惯用递归来实现循环。
与重复密切相关的是递归,在递归技术中,概念是直接或间接由其自身定义的。例如,我们可以通过“表要么为空,要么是一个元素后面再跟上一个表”这样的描述来定义表。很多编程语言都支持递归。在C语言中,函数F是可以调用自身的,既可以从F的函数体中直接调用自己,也可以通过一连串的函数调用,最终间接调用F。另一个重要思想——归纳,是与“递归”密切相关的,而且常用于数学证明中。
使用递归要注意的有两点:
1)递归就是在过程或函数里调用自身;
2)在使用递归时,必须有一个明确的递归结束条件,称为递归出口。
递归分为两个阶段:
1)递推:把复杂的问题的求解推到比原问题简单一些的问题的求解;
2)回归:当获得最简单的情况后,逐步返回,依次得到发杂的解。
斐波那契数列
1 int fib(int n) 2 3 { 4 5 if(0 == n) 6 7 return 0; 8 9 if(1 == n) 10 11 return 1; 12 13 if(n > 1) 14 15 return fib(n-1)+fib(n-2); 16 17 }
上面就是一个简单的递归调用了,由于递归引起一系列的函数调用,并且有可能会有一系列的重复计算,递归算法的执行效率相对较低。
递归调用实际上是函数自己在调用自己,而函数的调用开销是很大的,系统要为每次函数调用分配存储空间,并将调用点压栈予以记录。而在函数调用结束后,还要释放空间,弹栈恢复断点。所以说,函数调用不仅仅浪费空间,还浪费时间。
迭代(interation)是程序中对一组指令(或一定步骤)的重复。它即可以用作通用的术语(与“重复”同义),也可以用来描述一种特定形式的具有可变状态的重复。
计算机的威力源自其反复执行同一任务或同一任务不同版本的能力。在计算领域,迭代这一主题会以多种形式出现。数据模型中的很多概念(比如表)都是某种形式的重复,比如“表要么为空,要么由一个元素接一个元素,再接一个元素,如此往复而成”。使用迭代,程序和算法可以在不需要单独指定大量相似步骤的情况下,执行重复性的任务,如“执行下一步骤1000次”。编程语言使用像C语言中的while
语句和for
语句那样的循环结构,来实现迭代算法。
相比迭代,用递归解决这些问题来的更轻松,别人理解起你的代码也更加容易。但是递归有它自身的问题,每一次递归基本都需要在栈上申请一块新的空间,如果你干得漂亮的话用一个递归爆掉一个栈也不是很难的事情,除此之外,个人认为递归相对于迭代来说和计算机本身的设计原理有些不搭,同样的功能递归应该要慢一些。
有一种计算阶乘的方式,这里使用递归函数定义了计算阶乘的函数:
1 func factorial(n: Int) -> Int { 2 if n == 0 { 3 return 1 4 } 5 return n * factorial(n - 1) 6 }
现在我们试着描述这个函数的计算过程,以factorial(5)为例,一步步代换其计算过程。我们可以看到一个先逐步展开而后收缩的形状。在展开阶段里,这一计算过程构造起一个推迟进行的操作所形成的链条(在这里是一个乘法的链条),收缩过程表现为这些运算的实际执行。其形状可以描绘为如下的图例:
1 (factorial 5) 2 (5 * (factorial 4)) 3 (5 * (4 * (factorial 3)) 4 (5 * (4 * (3 * (factorial 2)) 5 (5 * (4 * (3 * (2 * (factorial 1))) 6 (5 * (4 * (3 * (2 * 1)))) 7 (5 * (4 * (3 * 2))) 8 (5 * (4 * 6)) 9 (5 * 24) 10 120
这样的计算过程是一个递归计算过程。递归计算过程由一个推迟执行的运算链条刻画,要执行递归计算过程,解释器就需要维护好那些以后要执行的操作的轨迹。
这种不同对于计算机而言却是重要的。在迭代的情况里,计算过程的任何一点,固定数目的状态变量都提供了有关计算状态的一个完整描述。而描述一个递归计算过程,需要一些“隐含”信息,它们并未保存在程序变量里,而是由解释器维持着,指明了在所推迟的运算所形成的链条里,计算过程正处于何处(这种解释器维持运算链条,需要使用一种称为栈的数据结构)。这个链条越长,需要保存的信息也就越多。
递归计算过程,通常容易理解,符合人类的思维习惯。但由于需要使用栈机制实现,其空间复杂度通常很高。对于一些递归层数深的计算,计算机会力不从心,空间上会以内存崩溃而告终。而且递归也带来了大量的函数调用,这也有许多额外的时间开销。所以在深度大时,它的时间复杂度和空间复杂度就都不好了。
迭代算法是用计算机解决问题的一种基本方法。它利用计算机运算速度快,适合做重复性操作的特点,让计算机对一组命令(或一定步骤)进行重复执行,在每次执行这组命令(或步骤)时,都从变量的原值退出它的一个新值。利用迭代算法解决问题,需要做好以下三个方面的工作:
(1)确定迭代变量。在可以用迭代算法解决的问题中,至少存在一个直接或间接地不断由旧值递推出新值的变量,这个变量就是迭代变量。
(2)建立迭代关系。所谓迭代关系,指如何从变量的前一个值推出其下一个值的公式(或关系)。迭代关系式的建立是解决问题的关键,通常可以使用递推或倒推的方法来完成。
(3)对迭代过程进行控制。在什么时候结束迭代过程?这是编写迭代程序必须考虑的问题。不能让迭代过程无休止地重复执行下去。迭代过程的控制通常可分为两种情况:一种是所需的迭代次数是个确定的值,可以计算出来;另一种是所需的迭代次数无法确定。对于前一种情况,可以构建一个固定次数的循环来实现对迭代过程的控制;对于后一种情况,需要进一步分析出用来结束迭代过程的条件。
递归是设计和描述算法的一种有力的工具,能采用递归描述的算法通常有这样的特征:为求解规模为N的问题,设法将它分解成规模较小的问题,然后从这些小问题的解方便地构造出大问题的解,并且这些规模较小的问题也能采用同样的分解和综合方法,分解成规模更小的问题,并从这些更小问题的解构造出规模较大问题的解。特别地,当规模N=1时,能直接得解。
递归算法的执行过程分递推和回归两个阶段。在递推阶段,把较复杂的问题(规模为n)的求解推到比原问题简单一些的问题(规模小于n)的求解。例如上例中,求解fib(n),把它推到求解fib(n-1)和fib(n-2)。也就是说,为计算fib(n),必须先计算fib(n-1)和fib(n- 2),而计算fib(n-1)和fib(n-2),又必须先计算fib(n-3)和fib(n-4)。依次类推,直至计算fib(1)和fib(0),分别能立即得到结果1和0。在递推阶段,必须要有终止递归的情况。例如在函数fib中,当n为1和0的情况。
在回归阶段,当获得最简单情况的解后,逐级返回,依次得到稍复杂问题的解,例如得到fib(1)和fib(0)后,返回得到fib(2)的结果,……,在得到了fib(n-1)和fib(n-2)的结果后,返回得到fib(n)的结果。
在编写递归函数时要注意,函数中的局部变量和参数知识局限于当前调用层,当递推进入“简单问题”层时,原来层次上的参数和局部变量便被隐蔽起来。在一系列“简单问题”层,它们各有自己的参数和局部变量。
由于递归引起一系列的函数调用,并且可能会有一系列的重复计算,递归算法的执行效率相对较低。当某个递归算法能较方便地转换成递推算法时,通常按递推算法编写程序。例如上例计算斐波那契数列的第n项的函数fib(n)应采用递推算法,即从斐波那契数列的前两项出发,逐次由前两项计算出下一项,直至计算出要求的第n项。
参考
http://note.zqguo.com/archives/301
http://www.ituring.com.cn/tupubarticle/5504#
http://lincode.github.io/Recursion-Iteration/
http://www.bianceng.cn/Programming/sjjg/200901/11200.htm
posted on 2015-11-26 19:29 qiaoshanzi 阅读(4070) 评论(0) 编辑 收藏 举报