从一个问题开始
假设A的推荐人是B,B的推荐人是C,如何来查找A的最终推荐人呢?
这里直接给出答案——
long findRootRerfererId(long actorId){
Long refererId = select referer_id from [table] where actor_id = actorId;
if(refererId == null)
return actorId;
return findRootRerfererId(refererId);
}
递归概述
递归是一种应用非常广泛的算法,亦可称作编程技巧。
递归的引出
递归顾名思义,包含递与归两个过程。我们用下面的例子来体会一下。
假设你去电影院看电影,你想知道自己坐在第几排,所以你询问前排的人,但前排的人有相同的困惑,他又去问他的前排,直到第一排的人明确自己坐在第几排。然后每一排的人将结果告诉自己的后排,整个递归的过程就完成了。
这个小例子中,第一排是终止条件,不断问前排的人是递归条件。无疑是很典型的递归问题,但面对递归问题更通常的反应是看了全会,写了全废。所以我们还要按部就班的学习递归的知识。。。
递归的三个条件
- 一个问题的解可以分解为几个子问题的解
- 设个问题与分解后的问题除了问题规模不同外,求解思路完全相同
- 存在递归终止条件
如何编写递归代码
编写递归代码的核心无非就两个:递归公式+终止条件。我们通过下面的小例子来理解下——
假设有n个台阶,每次你自己可以跨越1个台阶或者2个台阶。请问走这n个台阶共有多少种走法?
我们会想到,可以按照第一步的走法将问题分解为两个子问题,也就是走一步之后的走法和走两步之后的走法,对应的递归公式就是
f
(
n
)
=
f
(
n
−
1
)
+
f
(
n
−
2
)
f(n) = f(n-1)+f(n-2)
f(n)=f(n−1)+f(n−2)
再来确定终止条件,显然,当只有一个台阶时就没必要再递归了,也就是:
f
(
1
)
=
1
f(1)=1
f(1)=1
将这个递归终止条件带入递归公式后,发现f(2)是无法确定的,所以终止条件还应该包含
f
(
2
)
=
2
f(2)=2
f(2)=2
表示的含义是走两个台阶有两种走法。最后我们将其转换为代码实现——
int f(int n){
if(n == 1)
return 1;
if(n == 2)
return 2;
return f(n-1)+f(n-2);
}
站在巨人的肩膀——
写递归代码的关键在于找到将大问题转化为小问题的关键,并且基于此写下递归公式,然后再推敲递归终止条件,最终将递归公式和递归条件转化为代码实现。
递归代码要警惕堆栈溢出
递归代码在简洁可读性强的同时,却也带来了堆栈溢出的隐患。
之前提到过系统没调用一个函数,会将临时变量作为栈帧压栈,等到函数返回再弹栈。而递归代码因为调用的函数层数多,在函数调用时不断申请栈空间。从而导致空间复杂度不理想。
那怎样避免呢?理想的方法是根据场景选择递归,如果递归层数不多,则限制递归层数,这时使用递归是合适的。
递归代码要警惕重复计算
在刚才走台阶的例子中,就不可避免的遇到了重复计算的问题,这样对问题的求解并没有障碍,但并不优雅,我们要使用哈比表这样的数据结构动态存储每个子问题的结果。
一点思考
在问题规模较大时,有什么比较好的调试方式——
- 打印日志发现递归值
- 结合条件断点进行调试
日拱一卒,功不唐捐。