迭代、递归替代循环

循环(迭代)与递归的区别

1.    递归算法与迭代算法的设计思路区别在于:函数或算法是否具备收敛性,当且仅当一个算法存在预期的收敛效果时,采用递归算法才是可行的,否则,就不能使用递归算法。 

       当然,从理论上说,所有的递归函数都可以转换为迭代函数,反之亦然,然而代价通常都是比较高的。  

       但从算法结构来说,递归声明的结构并不总能够转换为迭代结构,原因在于结构的引申本身属于递归的概念,用迭代的方法在设计初期根本无法实现,这就像动多态的东西并不总是可以用静多态的方法实现一样。这也是为什么在结构设计时,通常采用递归的方式而不是采用迭代的方式的原因,一个极典型的例子类似于链表,使用递归定义及其简单,但对于内存定义(数组方式)其定义及调用处理说明就变得很晦涩,尤其是在遇到环链、图、网格等问题时,使用迭代方式从描述到实现上都变得很不现实。



2.   递归其实是方便了程序员难为了机器。它只要得到数学公式就能很方便的写出程序。优点就是易理解,容易编程。但递归是用栈机制实现的(c++),每深入一层,都要占去一块栈数据区域,对嵌套层数深的一些算法,递归会力不从心,空间上会以内存崩溃而告终,而且递归也带来了大量的函数调用,这也有许多额外的时间开销。所以在深度大时,它的时空性就不好了。 

循环其缺点就是不容易理解,编写复杂问题时困难。优点是效率高。运行时间只因循环次数增加而增加,没什么额外开销。空间上没有什么增加。



3.   局部变量占用的内存是一次性的,也就是O(1)的空间复杂度,而对于递归(不考虑尾递归优化的情况),每次函数调用都要压栈,那么空间复杂度是O(n),和递归次数呈线性关系。 



4.   递归程序改用循环实现的话,一般都是要自己维护一个栈的, 以便状态的回溯。 如果某个递归程序改用循环的时候根本就不需要维护栈, 那其实这个递归程序这样写只是意义明显一些,不一定要写成递归形式。 但很多递归程序就是为了利用函数自身在系统栈上的auto变量记录状态,以便回溯。 原理上讲,所有递归都是可以消除的,代价就是可能自己要维护一个栈。而且我个人认为,很多情况下用递归还是 必要的,它往往能把复杂问题分解成更为简单的步骤,而且很能反映问题的本质。



再来看看递归与递推的区别:



首先,递归和递推又一定的相似性(当然了,不然怎么会提出这个问题?)。这两个问题都可以描述为以下形式: f(n) = g(f(n-1),...,f(0)) 这是二者的共同特点。





不同点: 

1,从程序上看,递归表现为自己调用自己,递推则没有这样的形式。 

2,递归是从问题的最终目标出发,逐渐将复杂问题化为简单问题,最终求得问题是逆向的。 递推是从简单问题出发,一步步的向前发展,        最终求得问题。是正向的。 

3,递归中,问题的n要求是计算之前就知道的,而递推可以在计算中确定, 不要求计算前就知道n。 

4,一般来说,递推的效率高于递归(当然是递推可以计算的情况下)



由于一切递归问题都可以转化为循环求解,因此我们可以定义广义递归: 如果转化为循环后,需要自己维护堆栈,则仍称为是递归的。 在这个定义下,有些问题适用于用递归求解,如梵塔问题有些问题适用于用递推来做,如求满足N!>M条件时最小的N。有些问题二者都可以,如给定N时的阶乘问题。至于可读性,与问题有关,不能一概而论。





递归其实就是利用系统堆栈,实现函数自身调用。或者是相互调用的过程.在通往边界的过程中,都会把单步地址保存下来,知道等出边界,再按照先进后出的进行运算,这正如我们装木桶一样,每一次都只能把东西方在最上面,而取得时候,先放进取的反而最后取出。递归的数据传送也类似。但是递归不能无限的进行下去,必须在一定条件下停止自身调用,因此它的边界值应是明确的.就向我们装木桶一样,我们不能总是无限制的往里装,必须在一定的时候把东取出来。比较简单的递归过程是阶乘函数,你可以去看一下.但是递归的运算方法,往往决定了它的效率很低,因为数据要不断的进栈出栈。这时递推便表现出它的作用了,所谓递推,就是免除了数据进出栈的过程。也就是说,不需要函数不断的向边界值靠拢,而直接从边界出发,直到求出函数值。比如,阶乘函数中,递归的数据流动过程如下:   

f(3){f(i)=f(i-1)*i}-->f(2)-->f(1)-->f(0){f(0)=1}-->f(1)-->f(2)--f(3){f(3)=6}  而递推如下:   

f(0)-->f(1)-->f(2)-->f(3)   

  由此可见,递推的效率要高一些,在可能的情况下应尽量使用递推.但是递归作为比较基础的算法,它的作用不能忽视.所以,在把握这两种算法的时候应该特别注意.





















迭代、递归替代循环

计算机程序和构造一书里用递归和迭代替代循环,我用C#实现了下,经验证,结果正确。但是对于递归和迭代还是有点未弄清楚,感觉迭代里面其实也用了递归。敬请各位指教。

书中对递归过程是这么描述的:递归过程,论述的是一个语法形式上的事实,说明这个过程的定义中(直接或间接地)引用了该过程本身。而我们说某一计算过程具有某种模式时(比如说线性递归),我们说的是这一计算过程的进展方式,而不是相应过程书写上的语法形式。所以我们可以说某个递归过程将产生一个迭代的计算过程时,可能不好理解,但是这一计算过程确实是迭代的,如下述求两个例子的迭代法,他们其实是递归过程,但是其计算过程确实是迭代的。



一、不用循环,计算x+(x+1)+(x+2)+(x+3)+...+y

递归法:

代码

   /// <summary>

        /// 利用递归求和

        /// </summary>

        /// <param name="x">起始值</param>

        /// <param name="y">截止值</param>

        /// <returns>终值</returns>

        static int SumDigui(int x,int y)

        {

            if (x > y)

                return 0;

            else

            {

                return y + SumDigui(x, y - 1);

            }

        }

迭代法(递归过程,但是采用的是迭代的计算过程):

代码

        /// <summary>

        /// 迭代求和

        /// </summary>

        /// <param name="x">起始值</param>

        /// <param name="y">截止值</param>

        /// <returns></returns>

        static int SumDiedai(int x, int y)

        {

            return diedaiSum(x, x + 1, y);

        }

        /// <summary>

        /// 迭代求和

        /// </summary>

        /// <param name="product">结果=product+next</param>

        /// <param name="next"></param>

        /// <param name="maxcount"></param>

        /// <returns></returns>

        static int diedaiSum(int product, int next, int maxcount)

        {

            if (next > maxcount)

            {

                return product;

            }

            else

            {

                return diedaiSum(product + next, next + 1, maxcount);

             

            }

        }

二、不用循环,计算n的阶乘

递归法:

代码

 //递归求阶乘

        static int Jidigui(int n)

        {

            if (n == 1)

                return 1;

            else

            {

                return n * Jidigui(n - 1);

            }

        }

迭代法(递归过程,但是采用的是迭代的计算过程):

代码

 //迭代求阶乘

        static int Jidiedai(int n)

        {

            return factiter(1, 1, n);

        }

        /// <summary>

        /// 迭代计算

        /// </summary>

        /// <param name="product">结果=procduct*couter,初始值为1</param>

        /// <param name="couter">计数器counter初始值为1</param>

        /// <param name="maxcount">n</param>/// <returns></returns>

        static int factiter(int product, int couter, int maxcount)

        {

            if (couter > maxcount)

                return product;

            else

            {

                return factiter(product * couter, couter + 1, maxcount);

            }

        }

三、分别用递归和迭代求斐波那契数0,1123581321.........................

递归描述:

如果n=0,Fib(n)=0;

如果n=1,Fib(n)=1;

否则Fib(n)=Fib(n-1)+Fib(n-2);

程序如下:

代码

 static int Fib(int n)

        {

            if (n == 0)

            { return 0; }

            if (n == 1)

            { return 1; }

            else

            {

                return Fib(n - 1) + Fib(n - 2);

            }

        }

迭代描述:

设定一个基本整数a和一个基本整数b,初始化时a = Fib(1),b = Fib(0);

然后重复运用下列变换规则:

a = a + b;

b = a;

重复运用上述变换规则n此后,a = Fib(n+1);b = Fib(n)。

此时b就是我们要求的值:

代码如下:

代码

 //迭代法求斐波那契数

        static int FibDiedai(int n)

        {

            return FibIter(1, 0, n);

        }

        static int FibIter(int a, int b, int count)

        {

            if (count == 0)

                return b;

            else

            {

               return FibIter(a + b, a, count - 1);

            }

        }

由上述实例可以看出,迭代计算过程的状态可以用固定数目的状态变量描述,并且存在一套固定的规则描述计算过程从一个状态到下一状态的转换时,状态变量的更新方式,并且还可能存在描述这一计算过程应该终止的条件。
递归和迭代

      归与迭代都是基于控制结构:迭代用重复结构,而递归用选择结构。递归与迭代都涉及重复:迭代显式使用重复结构,而递归通过重复函数调用实现重复。递 归与迭代都涉及终止测试:迭代在循环条件失败时终止,递归在遇到基本情况时终止。使用计数器控制重复的迭代和递归都逐渐到达终止点:迭代一直修改计数器, 直到计数器值使循环条件失败;递归不断产生最初问题的简化副本,直到达到基本情况。迭代和递归过程都可以无限进行:如果循环条件测试永远不变成 false,则迭代发生无限循环;如果递归永远无法回推到基本情况,则发生无穷递归。



递归有许多缺点,它重复调用机制,因此重复函数调用的开销很大,将占用很长的处理器时间和大量的内存空间。每次递归调用都要生成函数的另一个副本 (实际上只是函数变量的另一个副本).从而消耗大量内存空间。迭代通常发生在函数内,因此没有重复调用函数和多余内存赋值的开销。那么,为什么选择递归 呢?



---------------------------------------------------------------------------

摘要:在算法的分析与设计中,递归和迭代都是特别有力的工具,很多难解的问题都是通过递归或迭代算法解出来的。本文在比较这两种算法在不同情况下的可行性的基础上,阐述了怎样对这两种算法进行有效的选择。 

关键词:递归算法 迭代算法 程序 

0 引言

在 算法分析与设计中,递归与迭代是我们解决循环问题常用的两种方法。那么,在既可以用递归算法又可以用迭代算法解决的问题中,我们究竟该选用哪种算法呢?在 程序设计中,我们不但讲求代码所能实现的功能,而且在实现相同功能的同时,更注重优化代码、提高代码的执行效率。这也是我们在选择递归还是迭代思想时考虑 的主要因素。

1 递归和迭代概述

如果一个问题刚开始难以解决,可以将其简化后再尝试解决。如果这个过程可以重复进行,问题最终会变得容易 处理。由此引出两种不同的方法:递归和迭代。循环或迭代,是一种重复执行一个过程的方法;递归是另一种方法。递归函数是通过调用函数自身来完成任务,而且 在每次调用自身时减少任务量。而迭代是循环的一种形式,这种循环不是由用户输入而控制,每次迭代步骤都必须将剩余的任务减少;也就是说,循环的每一步都必 须执行一个有限的过程,并留下较少的步骤。循环的进度通常用一个在每次迭代时都进行自增或自减的变量的值来衡量,直到到达预定的目标为止。用递归算法表示 许多问题的求解方法时算法思想非常简洁。但是递归算法不仅时间效率非常差,而且由于递归算法是不断的函数调用和函数返回过程,因此其实际的计算机运行时间 通常远大于循环方式算法的计算机运行时间,甚至在有限的时间内无法求解。这就存在一个把递归算法化为非递归算法的问题。

2 需要用迭代消解递归的情况 

递 归算法特别适合于所研究的问题或所处理的数据本身是递归定义的情况。然而,并不意味着这种递归定义保证递归算法是解决该问题的最好方法。事实上,主要是因 为拿那种不合适的例子来解释递归算法概念,从而造成了对程序设计中使用递归的普遍怀疑和否定态度,并把递归同低效等同起来。而且在递归算法中,往往会因为 追求代码短或者在求解问题时一味追求规律性,多用了无用的压栈和出栈的操作。比如用循环消解的尾递归,是多了无用的压栈和出栈才使速度受损的;斐波那契数 列计算的递归改循环迭代所带来的速度大幅提升,是因为改掉了重复计算的毛病。假使一个递归过程中本身包含了大量冗余的操作,并且这个过程又可以用迭代来达 到相同的效果。这时,我们就一般用迭代来消解递归。也就是说尾递归算法和单向递归算法可用迭代算法来代替。可以用一个方案来描述人们力图在其中避免使用算 法递归的程序,这个方案展示了其构成的模型。()式或等价的()式就是这个方案:

P≡ if B then (S;P)          (1)

P≡(S; if B then P)          (2)

要计算用简单递归关系定义的值时,这种方案是很自然的。如下例:

Function F (I: integer ):integer;

Begin if I>0 then F:=I*F(I-1)

Else F:=1;

End                                    (3)

很明显,在这种情况下,递归可由简单迭代代替,即由下面的程序代替。

I:=0;F:=1;

While I<n do

Begin I:=I+1;F:=I*F

End (4)

一般地,对应于方案()或()的程序应按照下列方案改写:

P ≡(x:=x0;while B do S)

还有更复杂的递归构造方案,这些方案可以并且应该改写成迭代计算的形式。一个例子就是Fibonacci 数的计算,这些数是按递归关系定义的:

FIBn+1=FIBn+FIBn-1对n>0

而FIB1=1,FIB0=0,一个直接的自然的解法导致程序

function fib (n: integer): integer;

begin if n=0 then fib :=0else

if n=1 then fib :=1 else 

fib:=fib(n-1)+fib(n-2)

end

以调用fib(n)来计算FIBn ,就引起这个函数过程的递归活动。频繁程度如何呢?我们注意到,对于n>1,每调用一次引起两个新的调。因此,调用的总次数按指数增长,如下图所示fib(5)的递归树。这样一个程序显然是不实用的。

但显见,利用辅助变量x=FIBi与y=FIBi-1,就可以用迭代方案来计算Fibonicca 数,而避免同一值的重复计算。

{ 对n>0计算x=FIBn}

i:=1;x:=1;y:=0;

while i<n do

begin z:=x; i:=i+1;

x:=x + y; y:=z

end

(注意,对x, y, z的三个赋值,可以只表示成两个赋值,而不需要辅助变量z :x:=x +y ; y:=x-y).由上述例子可以看出,当存在明显的迭代解法时要避免使用递归。

3 不需要消解的递归

那 种盲目的消解递归,不惜一切代价躲避递归,认为“递归的速度慢,为了提高速度,必须用栈或者其他的方法来消解”的说法是很片面的。如果一个递归过程用非递 归的方法实现后,速度提高了,那只是因为递归做了一些无用功。假使一个递归过程必须要用栈才能消解,那么完全模拟后的结果根本就不会对速度有任何提升,只 会减慢;如果你改完后速度提升了,那只证明你的递归函数写的有问题,如多了许多重复操作——打开关闭文件、连接断开数据库,而这些完全可以放到递归外面。 可以在本质上是非递归的机器上实现递归过程这一事实本身就证明:为着实际目的,每一个递归程序都可以翻译成纯粹迭代的形式,但这包含着对递归栈的显式处 理,而这些运算常常模糊了程序的本质,以致使它非常难以理解。 

因此,是递归的而不是迭代的算法应当表述成递归过程。如汉诺塔问题等。汉诺塔问 题的递归算法中有两处递归调用,并且其中一处递归调用语句后还有其他语句,因此该递归算法不是尾递归或单向递归。要把这样的递归算法转化为非递归算法,并 没有提高程序运行的速度,反而会使程序变得复杂难懂,这是不可取的。也就是说,很多递归算法并不容易改写成迭代程序:它们本质上是递归的,没有简单的迭代 形式。这样的递归算法不宜转化为非递归算法。

4 结束语

说到底,在我们选择算法时应该全面分析算法的可行性、效率、代码优化。在综合了算法的各个因素后,选择合适的算法来编写程序,这样的程序才会达到优化的效果。

参考文献:

[]王晓东.算法分析与设计清华大学出版社.

[]Alice E. Fischer DavidW .Egger 等著Applied C: An Introduction and More电子工业出版社.

 

你可能感兴趣的:(递归)