目录
1 什么是递归
1.1 递归定义
所谓递归,简单点来说,就是一个函数直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。就以阶乘函数为例,相应代码如下所示:
/**
1. 阶层函数
2. @param n
3. @return
*/
public static int Factorial(int n) {
if (n==1 || n==0) {
return 1;
} else {
return n*Factorial(n-1);
}
}
首先我们必须明白函数的目的(意图),有哪些条件。然后找出相应的逻辑关系,并判断终止条件。最后才能实现相关递归方法。
阶层函数很简单,求n的阶层,意图知道了,n作为条件也知道了。之后就是找相应的逻辑关系,这里的逻辑关系也非常简单,n的阶层等于n乘以(n-1)的阶层。并且我们明显知道n=1或0的阶层是1。方法就找出来了,如上。
1.2 递归的实现步骤
由上文的递归定义我们大概知道了递归函数是怎么求的。相应的步骤如下所示:
- 首先了解意图,需要做什么,需要求什么。
- 了解问题之间的逻辑关系。
- 把这种逻辑关系最容易知道的作为判断条件。
- 写函数。
是的,求一个问题的递归函数,就是这4大步骤,但是实现起来灵活多变,本文在此小结不作过多的探讨。
1.3 递归的分类
首先我们知道递归的种类有5种,分别是线性递归、二分递归、尾递归、互递归、嵌套递归。
1.3.1 线性递归
它通过一种简单的方式调用自己,并通过一个终止条件终止自身,终止条件是我们已知的条件。如上文的阶层代码就属于线性递归。
1.3.2 二分递归
它通过二次递归的调用自己,并通过一个终止条件终止自身,终止条件是我们已知的条件。比如斐波那契数列,其递归逻辑是f(n)=f(n-1)+f(n-2)。确实是两次,您说呢?
1.3.3 尾递归
尾递归就是把次顶层获取到数据以参数的形式传递到顶层。对这里必须是以参数的形式传递。其表现就是最终的结果是其相应参数的组合逻辑(包括简单逻辑)。就以阶层函数用尾递归实现。
/**
* 尾递归实现阶层函数
* @param n
* @param result
* @return
*/
public static int Factorial(int n, int result) {
if (n==1) {
// 没错最终的结果就是相应参数的逻辑组合(这里是最简单的逻辑,直接本身)
return result;
} else {
return Factorial(n-1, n*result);
}
}
1.3.4 互递归
函数相互调用。反正我觉得没什么用途。
1.3.5 嵌套递归
函数调用时,以递归函数作为参数进行调用。比如最值函数递归。
2 怎么实现递归
上一章主要讲了递归的定义、分类、实现的主要思路。本小节将对递归实现的思路做深入的探讨。
2.1 尾递归与其他递归的区别
没错尾递归最终的结果与相应的参数的逻辑组合对应。就以阶层函数为例:f(n,result) = f(n-1,n*result)。如此总共迭代n次,发现f(n,result)=f(0,1*2…*n*result),而且发现最终的结果不需要回调,因为最终结果就是result*n*n-1…*1;这就是普通的for循环函数,节约了时间复杂度。
2.1.1 递归优化
对,尾递归就是能优化递归时间复杂度。
2.2 动态规划的实质
在我看来动态规划的实质也就是一种递归,相应的转移方程为其递归公式。就01背包问题为例。相应的实现思路如下所示:
2.2.1 动态规划迭代实现
/**
* 背包最大值
* 状态转移方程 dp[i][k] = max(value[i] + dp[i-1][k-weight[i]], dp[i-1][k])
* @param weight
* @param value
* @param w
* @return
*/
public static int maxValue(int[] weight, int[] value, int w) {
int length = weight.length;
if (length == 0) {
return 0;
} else {
int[][] dp = new int[length+1][w+1];
for (int i = 1; i <= length; i++) {
for (int k = 1; k <= w; k++) {
// 存放i号物品
int first = (k>=weight[i-1])?value[i-1] + dp[i-1][k-weight[i-1]]:0;
// 不存放i号物品
int second = dp[i-1][k];
dp[i][k] = Math.max(first, second);
}
}
return dp[length][w];
}
}
2.2.2 动态规划递归实现
/**
* 对数组中前length个数进行判断选取,取得的最大价值
* @param weight
* @param value
* @param length
* @param weights
* @return
*/
public static int beibao(int[] weight, int[] value, int length, int weights) {
if (weights <= 0 || length<=0) {
return 0;
} else{ // 这里就是动态规划的状态转移方程
// 不选取
int first = beibao(weight, value, length-1, weights);
// 选取,但是必须判断
int second = 0;
if (weights>=weight[length-1]) {
second = beibao(weight, value, length-1, weights-weight[length-1])
+ value[length-1];
}
return Math.max(first, second);
}
}
2.2.3 动态规划尾递归实现
/**
* o1背包问题尾递归实现
* 对所有的物品进行逐一判断是否选择
* @param weight 物品质量集
* @param value 物品价值集
* @param num 对物品进行判断的个数
* @param result 返回结果
* @param weights 背包容量
* @return
*/
public static int beibao(int[] weight, int[] value, int num, int result, int weights) {
// 背包容量不足
if (weights < 0) {
return 0;
}
// 当所有的物品进行判断选择了
int length = weight.length;
if (num == length) {
return result;
} else {
// 继续向下判断,分两种情况。选择下一个物品,与不选择下一个物品
// 选择
int first = beibao(weight, value, num+1, result+value[num],
weights-weight[num]);
// 不选择
int second = beibao(weight, value, num+1, result, weights);
// 返回的是两种情况的最大值
return Math.max(first, second);
}
}
2.3 由动态规划引入递归问题的深入思考
上文中,01背包问题,分别用了3种方式实现,动态规划、递归、尾递归。但是在用递归和尾递归的时候,本文是从不同角度来实现的。简单说,上文01背包问题递归实现是从整体到个体进行递归,而上文01背包问题尾递归实现确实从个体到整体的思维进行递归。或许你不懂什么叫整体到个体、个体到整体。也就是考虑问题是从整体出发、或从个体出发。也就是01背包问题实现递归的主要方法分为个体到整体递归方法、整体到个体递归方法、个体到整体尾递归方法、整体高个体尾递归方法四大类。上文中01背包问题第一个是用了整体到个体递归方法,其对应的整体到个体尾递归实现如下所示:
public static int beibao(int[] weight, int[] value, int length, int result, int weights) {
// 背包容量不足
if (weights<0) {
return 0;
}
// 如果数组的有效长度为0,直接返回
if (length == 0) {
return result;
} else {
// 不选取
int first = beibao(weight, value, length-1, result, weights);
// 选取,但是必须判断
int second = 0;
if (weights>=weight[length-1]) {
second = beibao(weight, value, length-1, result-weight[length-1],
weights-weight[length-1]) + value[length-1];
}
return Math.max(first, second);
}
}
我发现整体到个体的尾递归会出现一些微小的偏差,但我敢肯定,这样没错,如用人工手段,进行迭代也是这样的结果。
知道了,当数组长度为0时,返回的结果肯定是0,但是上文中返回result,一般情况下这个result也是0,但是不知道什么原因总是-1。确实现在想想也是,result是变量,怎么能在终止条件上复制变量呢?
总结
发现递归就是这么简单,总共四大类,个人建议递归用整体到个体的逻辑思路,尾递归用个体到整体的思路。