递归
理解场景
递归其实主要有两个关键,第一个就是要可以递,第二个就是要可以归,其实也就是有去有回。
可以想象这样一个场景,你面前有一个门,你手里拿着一把钥匙,你可以用这把钥匙打开这个门,然后打开这个门之后,你发现前面还有一扇门,但是这扇门也可以用你手中的钥匙打开,就这样你一直向里面打开了很多扇门,知道你打开最后一扇门,发现门里面是一睹墙。然后你带着你得到的东西返回到最初的位置。
递归解决问题的时候,可以在递去的过程中处理问题,如下:
function recursion(大规模){
if (end_condition){ // 明确的递归终止条件
end; // 简单情景
}else{ // 在将问题转换为子问题的每一步,解决该步中剩余部分的问题
solve; // 递去
recursion(小规模); // 递到最深处后,不断地归来
}
}
也可以在归来的过程中处理问题,如下:
function recursion(大规模){
if (end_condition){ // 明确的递归终止条件
end; // 简单情景
}else{ // 先将问题全部描述展开,再由尽头“返回”依次解决每步中剩余部分的问题
recursion(小规模); // 递去
solve; // 归来
}
}
递归是在什么时候处理问题,我们可以通过方法里面调用自身的代码的位置来判断,看看解决问题的代码是在调用自身代码的位置之前还是在之后,如果在之前,那么递归就是在递的过程中解决问题,如果是在之后,那么递归就是在归的过程中解决问题。
递归需要的三个条件
明确递归的终止条件
我们知道,递归就是有去有回,既然这样,那么必然应该有一个明确的临界点,程序一旦到达了这个临界点,就不用继续往下递去而是开始实实在在的归来。换句话说,该临界点就是一种简单情境,可以防止无限递归。
给出递归终止时的处理办法
我们刚刚说到,在递归的临界点存在一种简单情境,在这种简单情境下,我们应该直接给出问题的解决方案。一般地,在这种情境下,问题的解决方案是直观的、容易的。
提取重复的逻辑,缩小问题规模
我们在阐述递归思想内涵时谈到,递归问题必须可以分解为若干个规模较小、与原问题形式相同的子问题,这些子问题可以用相同的解题思路来解决。从程序实现的角度而言,我们需要抽象出一个干净利落的重复的逻辑,以便使用相同的方式解决子问题。
递归例子
例子1
下面的这个例子是在递的过程中处理问题,如下图:
private DepartmentVO findDepartmentVO(DepartmentVO result, Long id) {
if (result.getId().equals(id)) {
return result;
}
if (CollectionUtils.isEmpty(result.getSubDepartments())) {
return null;
}
for (DepartmentVO subDepartment : result.getSubDepartments()) {
DepartmentVO departmentVO = findDepartmentVO(subDepartment, id);
if (departmentVO != null) {
return departmentVO;
}
}
return null;
}
上面的代码的逻辑,是在一个部门树里面搜索某个部门,result是一个部门树,id是要搜索的部门id。上面这个递归是在递的过程中处理的问题,上面的这个例子很好的说明了递归所需要的三个条件。
例子2
下面的这个例子,也是在递的过程中解决问题:
public static long f(int n){
if(n == 1) // 递归终止条件
return 1; // 简单情景
return n*f(n-1); // 相同重复逻辑,缩小问题的规模
}
上面的这个递归,也满足递归的三个条件:
1.终止条件,也就是if(n==1)
2.终止的时候的处理办法,也就是return n*f(n-1),其实也就是返回一个结果
3.抽取重复的逻辑,缩小问题的规模,其实也就是方法中上面的代码
总结
从上面的两个例子中可以看出,递归肯定是先递,然后再归。终止时的处理办法,其实也就是归来的结果。无论你是在递的过程中处理的问题,还是在归的过程中处理的问题,在最后归来的时候,你必须要有一些东西带回来,这就是递归。
什么时候可以用到递归
一个大问题,如果它可以划分成多个小的问题,并且这些小的问题可以用同一种方式解决,那么我们就可以用到递归。