递归的思想
程序调用自身的编程技巧称为递归.。
简单来说,就是一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题(子问题)来求解。因为解决原问题和解决子问题往往是同一个方法,所以形成了自己调用自己的情况。递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归需具备的条件
- 子问题须与原始问题为同样的事,且更为简单
- 不能无限制地调用本身,必须有个出口,否则会出现栈溢出。
例子: 计算正整数n的阶乘
- 原问题: 计算n!
- 子问题: 计算(n-1)!
- 联系: n!=n*(n-1)!
- 出口: n=1时, 结果为1
代码如下:
public class factorial {
public static void main(String[] args) throws Exception {
int num = digui(5);
System.out.println(num);
}
public static int digui(int n ) throws Exception {
if(n<0){
throw new Exception("参数不能为负数!");
}else if(n == 1 || n == 0){ //定义递归的出口
return 1;
}else{
return n *digui(n-1);
}
}
}
递归的过程
在求解n =5的阶乘时,递归过程如下
我们发现,递归过程和栈的工作原理一致。整个过程实际就是一个栈的入栈与出栈问题,先进后出。
例子:斐波那契数列
斐波那契数列指的是:0、 1、1、2、3、5、8、13…
递归关系表达式: F(n) = F(n-1)+F(n-2), n为>=的整数
代码如下
public static int fib_dg(int n ){
if(n==0){
return 0;
}else if(n==1){
return 1;
}else{
return fib_dg(n-1)+fib_dg(n-2);
}
}
递归的过程
可以发现,递归一般是自顶向下的形式编写的,比如F(5),向下逐渐分解规模,直到到达了f(1)和f(0)这两个出口,然后逐层返回答案,但是在这个过程中,存在大量的重复计算,导致时间复杂度达到了2^n. 我们以下两种方法解决这个问题
- 带有备忘录的递归算法
- 自底向上法
带有备忘录的递归算法
直接利用递归求解耗时的原因是重复计算,那么我们可以建立一个【备忘录】,每次算出某个子问题的答案后别急着返回,先记到【备忘录】里再返回,每次遇到一个子问题先去【备忘录】里查一查,如果发现之前已经解决过这个问题了,直接把答案拿出来用,不要再耗时去计算了.
代码如下
public class factorial {
public static int[] memo ;
public static void main(String[] args) throws Exception {
int n = 20;
memo = new int[n];
int fib_dg = fib_dg_memo(20);
System.out.println(Arrays.toString(memo));
System.out.println(fib_dg);
}
public static int fib_dg_memo(int n ){
if(n==0){
return 0;
}else if(n==1){
return 1;
}else if(memo[n-1]!=0){
return memo[n-1];
}else{
memo[n-1] = fib_dg_memo(n-1)+fib_dg_memo(n-2);
return memo[n-1];
}
}
}
通过带有备忘录的递归,我们可以发现运算时间大大减短了。
自底向上法
我们直接从最底下,问题规模最小的F(1)和F(2)开始往上推,直到推到我们想要的答案
代码如下:
public static void fib_zdxs(int n){
int[] arr = new int[n];
if(n<=2) {
return;
}
arr[0] = 1;
arr[1] = 1;
for (int i = 2; i <n ; i++) {
arr[i] = arr[i-1]+arr[i-2];
}
System.out.println(Arrays.toString(arr));
}
自底向上法是最简单的,也是我们最常用的。
总结
- 递归运用的场景非常多,比如深度优先遍历,广度优先遍历,树、图
- 学好递归,不单单是掌握这一项技能,而是思维的转变。大家抓紧学好递归。
- 下篇,我们将来讲动态规划。
引用: 数学建模清风.