深入理解和设计递归技术的关键点的思考

馋奶兔 提交于 2020-02-25 01:11:42

如何理解递归,写出正确的递归程序,我觉得有几个关键点:

1.要从整体把握问题

递归的难点在于人脑并不适合去跟踪递归中自己调用自己的这个过程,这是因为人脑中不像计算机一样有一个可以记忆的堆栈,

但是不同的是人是会归纳的,而计算机只知道调用-返回。

因此,理解和设计递归,一定要从整体把握,数学归纳法和递归是一个对称的关系,数学归纳法不断的扩展自己,递归则是不断的分解

自己。递归中的递就是把主问题分解成子问题,归就是利用子问题的解逐步向上求解的过程了。

关键点是要善用数学归纳法里面的假设,就是假设子问题已经求解了,它得到的结果是什么,一旦这么想就是在从整体上把握问题,不必再去纠结细节。设计一个递归函数的时候,首先像普通函数一样设计一个算法框架,控制好程序逻辑,处理递归调用的时候假设是在调用一个其他的函数,这个函数已经设计好,你只需要知道他做什么事,然后返回什么就可以了,具体怎么做的,不需要去想,这似乎有些矛盾,因为这个函数本来就是自己,怎么不需要去想怎么做的呢?其实这就是递归难以理解的原因。所以必须先把细节放在一边,先确定好框架,然后再去处理细节。

2.关注函数的返回值是什么,如何利用子函数调用的返回值得到调用的返回值。

第一条里面设计好框架以后,然后我们就需要关注函数处理的细节了,这个细节包括流程分支,函数的返回值。

函数的返回值直接关系到函数是否正确执行,因为函数返回什么你的递归子调用就会返回什么,而递归子调用的返回会影响到最终结果,

因此必须关注函数的返回。子程序返回的结果被调用者所使用,调用者又会返回,因此存在一个问题:那就是函数返回的一致性。

因为一般来说,复杂一点的递归函数设计会涉及到很多逻辑分支,这些逻辑分支的返回一定要保持一致,即不同情况下的函数的解。

这一点很容易出错,递归的原理基于子问题,子问题只是一个小的规模的父问题,因为我们是假设子问题能够求解的,而父问题的解由子问题的解组成,所以父问题和子问题应该解决的是同一个问题,他们的结果应该是一致的。


 

递归的使用条件:

  存在一个递归调用的终止条件;

  每次递归的调用必须越来越靠近这个条件;只有这样递归才会终止,否则是不能使用递归的!

总之,在你使用递归来处理问题之前必须首先考虑使用递归带来的好处是否能补偿他所带来的代价!否则,使用迭代算法会比递归算法要高效。

 

递归的基本原理:

  1 每一次函数调用都会有一次返回.当程序流执行到某一级递归的结尾处时,它会转移到前一级递归继续执行.

  2 递归函数中,位于递归调用前的语句和各级被调函数具有相同的顺序.如打印语句 #1 位于递归调用语句前,它按照递归调用的顺序被执行了 4 次.

  3 每一级的函数调用都有自己的局部变量.

  4 递归函数中,位于递归调用语句后的语句的执行顺序和各个被调用函数的顺序相反.

           即位于递归函数入口前的语句,右外往里执行;位于递归函数入口后面的语句,由里往外执行。

  5 虽然每一级递归有自己的变量,但是函数代码并不会得到复制.

  6 递归函数中必须包含可以终止递归调用的语句.

 

 一旦你理解了递归(理解递归,关键是脑中有一幅代码的图片,函数执行到递归函数入口时,就扩充一段完全一样的代码,执行完扩充的代码并return,继续执行前一次递归函数中递归函数入口后面的代码),阅读递归函数最容易的方法不是纠缠于它的执行过程,而是相信递归函数会顺利完成它的任务。如果你的每个步骤正确无误,你的限制条件设置正确,并且每次调用之后更接近限制条件,递归函数总是能正确的完成任务。

不算递归调用语句本身,到目前为止所执行的语句只是除法运算以及对quotient的值进行测试。由于递归调用这些语句重复执行,所以它的效果类似循环:当quotient的值非零时,把它的值作为初始值重新开始循环。但是,递归调用将会保存一些信息(这点与循环不同),也就好是保存在堆栈中的变量值。这些信息很快就会变得非常重要。

 

斐波那契数列是典型的递归案例:

 

  Fib(0) = 0 [基本情况] Fib(1) = 1 [基本情况]

       对所有n > 1的整数:Fib(n) = (Fib(n-1) + Fib(n-2)) [递归定义]

 

递归算法一般用于解决三类问题:

 

  (1)数据的定义是按递归定义的。(Fibonacci函数)

  (2)问题解法按递归算法实现。(回溯)

  (3)数据的结构形式是按递归定义的。(树的遍历,图的搜索)

如:

 

  procedure a;

  begin

  a;

  end;

  这种方式是直接调用.

 

  又如:

  procedure b;

  begin

  c;

  end;

 

  procedure c;

  begin

  b;

  end;

 

  这种方式是间接调用.

 

如何设计递归算法 
  1.确定递归公式 
  2.确定边界(终了)条件

 

 

新想法:

 

        1,如果递归函数有返回值,则必须把递归函数入口整合到函数中去。

结合例子来说明:

这是一个python写的BST的删除函数:

bstRemove函数的主框架:

1 如果subtree为None,返回subtree

2 如果小于key ,在左子树中执行bstRemove,返回subtree

3 如果大于key,在右子树中执行bstRemove ,返回subtree

4 如果等于key,执行删除的算法,返回subtree

主框架总共有四个分支,根据subtree的值来确定具体走向哪一个分支。

这个函数有两个参数一个是subtree,还有一个是target,这个函数的作用是从subtree这个node开始搜索并删除一个值等于target的节点,最后返回根节点。

关键点就是这个函数返回的根节点是什么:

这个根节点是一颗子树的根节点,这颗子树已经执行了bstRemove,他的结构可能已经改变,根节点可能已经被替换所以要

subtree.left = self._bstRemove( subtree.left, target )  将根节点重新付给subtree.left,搞清楚这一点其他就好理解了。

整个函数的目的就是删除一颗树的节点,然后返回整个树的根。

这里返回值的一致性就是---你总是应该返回整个树的根。

然后我们仔细看看各个分支,他们的返回值都遵循了这一点,假设子问题已经解决,得到根节点,然后再怎样怎样解决主问题,然后返回当前树的根节点。

关注了这一点,也就明白递归是如何像数学归纳法一样从n-1得到n了。

 

参考网址:

http://www.cnblogs.com/zhangqqqf/archive/2008/09/12/1289730.html    //包含汉诺塔问题

http://dev.yesky.com/171/3064671.shtml

http://www.ibm.com/developerworks/cn/linux/l-recurs.html    //IBM

http://baike.baidu.com/view/96473.htm#2                           //包含习题

http://www.rupeng.com/forum/thread-19296-1-1.html

http://blog.csdn.net/ysuncn/archive/2007/09/21/1793896.aspx

http://www.rupeng.com/forum/thread-19241-1-1.html

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!