理解和设计递归的关键点的思考
如何理解递归,写出正确的递归程序,我觉得有几个关键点:
1.要从整体把握问题
递归的难点在于人脑并不适合去跟踪递归中自己调用自己的这个过程,这是因为人脑中不像计算机一样有一个可以记忆的堆栈,
但是不同的是人是会归纳的,而计算机只知道调用-返回。
因此,理解和设计递归,一定要从整体把握,数学归纳法和递归是一个对称的关系,数学归纳法不断的扩展自己,递归则是不断的分解
自己。递归中的递就是把主问题分解成子问题,归就是利用子问题的解逐步向上求解的过程了。
关键点是要善用数学归纳法里面的假设,就是假设子问题已经求解了,它得到的结果是什么,一旦这么想就是在从整体上把握问题,不必再去纠结细节。设计一个递归函数的时候,首先像普通函数一样设计一个算法框架,控制好程序逻辑,处理递归调用的时候假设是在调用一个其他的函数,这个函数已经设计好,你只需要知道他做什么事,然后返回什么就可以了,具体怎么做的,不需要去想,这似乎有些矛盾,因为这个函数本来就是自己,怎么不需要去想怎么做的呢?其实这就是递归难以理解的原因。所以必须先把细节放在一边,先确定好框架,然后再去处理细节。
2.关注函数的返回值是什么,如何利用子函数调用的返回值得到调用的返回值。
第一条里面设计好框架以后,然后我们就需要关注函数处理的细节了,这个细节包括流程分支,函数的返回值。
函数的返回值直接关系到函数是否正确执行,因为函数返回什么你的递归子调用就会返回什么,而递归子调用的返回会影响到最终结果,
因此必须关注函数的返回。子程序返回的结果被调用者所使用,调用者又会返回,因此存在一个问题:那就是函数返回的一致性。
因为一般来说,复杂一点的递归函数设计会涉及到很多逻辑分支,这些逻辑分支的返回一定要保持一致,即不同情况下的函数的解。
这一点很容易出错,递归的原理基于子问题,子问题只是一个小的规模的父问题,因为我们是假设子问题能够求解的,而父问题的解由子问题的解组成,所以父问题和子问题应该解决的是同一个问题,他们的结果应该是一致的。
结合例子来说明:
这是一个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了。