编写递归代码的关键是:只要遇到递归,我们就把它抽象成一个递推公式,不用想一层层的调用关系,不要试图用人脑去分解递归的每个步骤。(但是我觉得简单的递归过程,比如二叉树上的dfs类型的题目,自己要有能画图讲解的能力,有助于你自己的理解,而不是简单的背几个条件,要具体的知道比如到了叶子节点会发生什么?null节点?递归返回的过程?)
递归需要注意的问题:
- 如何解决递归代码的堆栈溢出?
我们可以通过在代码中限制递归调用的最大深度的方式来解决这个问题。递归调用超过一定深度(比如 1000)之后,我们就不继续往下再递归了,直接返回报错。
int depth = 0;
int fun(int n){
depth++;
if(depth > 10000) throw -1;
...
}
但这种做法并不能完全解决问题,因为最大允许的递归深度跟当前线程剩余的栈空间大小有关,事先无法计算。如果实时计算,代码过于复杂,就会影响代码的可读性。所以,如果最大深度比较小,比如 10、50,就可以用这种方法,否则这种方法并不是很实用。
- 警惕重复计算
比如一个树形结构,从上到下的就计算,可能结点node1计算的时候已经算了下面结点node2的值,然而在递归下去时仍然计算node2的值,造成重复计算。为了避免重复计算,我们可以通过一个数据结构(比如散列表)来保存已经求解过的 f(k)。当递归调用到 f(k) 时,先看下是否已经求解过了。如果是,则直接从散列表中取值返回,不需要重复计算,这样就能避免刚讲的问题了。
除此之外,注意递归代码的其他问题:在时间效率上,递归代码里多了很多函数调用,当这些函数调用的数量较大时,就会积聚成一个可观的时间成本。在空间复杂度上,因为递归调用一次就会在内存栈中保存一次现场数据,所以在分析递归代码空间复杂度时,需要额外考虑这部分的开销。
如何解决递归成环的情况呢?
用一个c++的set,引用传递,每次就查找一下即可。
课后思考
我们平时调试代码喜欢使用 IDE 的单步跟踪功能,像规模比较大、递归层次很深的递归代码,几乎无法使用这种调试方式。对于递归代码,你有什么好的调试方法呢?
方法1:打印日志发现,递归值。
方法2:结合条件断点进行调试。