递归和动态规划的理解和感悟

首先给大家分享一句话,

To Iterate is Human, to Recurse, Divine.+

人理解迭代,神理解递归。
                                                                                                                                                                                          ----- L. Peter Deutsch

由此可见,递归虽然简洁能给我们省下很多代码,但是也特别难理解,接下来,我将用几道题来阐述一下我对递归和动态规划的理解,如果有错误的地方,还请大家指正。

递归是什么?

  • 递+归
  • 程序调用自身的编程技巧
    大家应该做过很多关于递归的问题,和了解过递归的定义,所以我在这里就不对递归做太多的解释了。等一下直接看题吧。

我认为递归方法必须包含的两个要素:

  • 递归体,这里面需要你写主要的逻辑代码
  • 递归出口,这里需要写递归方法的终止条件,防止递归无限循环。

递归的优点:

  • 代码简短清晰

我感觉这道题就能体现出来递归代码简短清晰的优点。
题目链接
这道题让我们求的是字符串的字母组合,如果我们用for循环写。
如果输入的是2,那么对应的字符串是"abc",我们可以这样写:

public class t1 {
    static String[] strings = {"abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
    public static void main(String[] args) {
        for (int i = 0; i < strings[0].length(); i++) {
            System.out.println(strings[0].charAt(i));
        }
    }
}

如果输入的是23,那么对应的字符串是"abc",“def”,我们可以这样写:

public class t1 {
    static String[] strings = {"abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
    public static void main(String[] args) {
        for (int i = 0; i < strings[0].length(); i++) {
            for (int j = 0; j < strings[1].length(); j++) {
            System.out.println(strings[0].charAt(i)+""+strings[1].charAt(j));
        }}
    }
}

如果输入的是234,那么对应的字符串是"abc",“def”,“ghi”,我们可以这样写:

public class t1 {
    static String[] strings = {"abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};

    public static void main(String[] args) {
        for (int i = 0; i < strings[0].length(); i++) {
            for (int j = 0; j < strings[1].length(); j++) {
                for (int k = 0; k < strings[2].length(); k++) {
                    System.out.println(strings[0].charAt(i) + "" + strings[1].charAt(j) + "" + strings[2].charAt(k));
                }
            }
        }
    }
}

由此我们可以得出一个结论,字符串对应的长度就是for循环嵌套的层数,但是输入的字符串的长度是不固定的,这时候就该递归出场了。

public class t1 {
    static String[] strings = {"abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        String s = scanner.nextLine();
        StringBuilder stringBuilder = new StringBuilder("");
        f(s,0,stringBuilder);//首先,我们要把输入的字符串传入,并做一个标记,代表这是第几个for循环,最后一个字符串用来收集。
    }

    private static void f(String s, int k,StringBuilder temp) {
        if(k==s.length()){
            System.out.println(temp.toString());
            return;
        }
        int num = s.charAt(k)-'2';
        for (int i = 0; i < strings[num].length(); i++) {
            f(s,k+1,temp.append(strings[num].charAt(i)));
            temp.deleteCharAt(temp.length()-1);
        }
    }
}

这道题还有一种解法是利用队列求解:

import java.util.ArrayList;
import java.util.List;

public class t {
    public static void main(String[] args) {
        System.out.println(letterCombinations("23"));
    }
    public static List<String> letterCombinations(String digits) {
        if(digits==null || digits.length()==0) {
            return new ArrayList<>();
        }
        //一个映射表,第二个位置是"abc“,第三个位置是"def"。。。
        //这里也可以用map,用数组可以更节省点内存
        String[] letter_map = {
                " ","*","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"
        };
        List<String> res = new ArrayList<>();
        //先往队列中加入一个空字符
        res.add("");
        for(int i=0;i<digits.length();i++) {
            //由当前遍历到的字符,取字典表中查找对应的字符串
            String letters = letter_map[digits.charAt(i)-'0'];
            int size = res.size();
            //计算出队列长度后,将队列中的每个元素挨个拿出来
            for(int j=0;j<size;j++) {
                //每次都从队列中拿出第一个元素
                String tmp = res.remove(0);
                //然后跟"def"这样的字符串拼接,并再次放到队列中
                for(int k=0;k<letters.length();k++) {
                    res.add(tmp+letters.charAt(k));
                }
            }
        }
        return res;
    }
}

感觉这个递归的方法比队列求解的方法简短清晰很多。

当然递归的缺点也很明显:

  • 递归由于是函数调用自身,而函数调用是有时间和空间的消耗的:每一次函数调用,都需要在内存栈中分配空间以保存参数、返回地址以及临时变量,而往栈中压入数据和弹出数据都需要时间。->效率

  • 递归中很多计算都是重复的,由于其本质是把一个问题分解成两个或者多个小问题,多个小问题存在相互重叠的部分,则存在重复计算,如fibonacci斐波那契数列的递归实现。->效率

  • 调用栈可能会溢出,其实每一次函数调用会在内存栈中分配空间,而每个进程的栈的容量是有限的,当调用的层次太多时,就会超出栈的容量,从而导致栈溢出。->性能

缺点总结:
由于递归需要系统堆栈,所以空间消耗要比非递归代码要大很多。而且,如果递归深度太大,可能系统撑不住。

有一个很好的例子就是爬楼梯问题
题目链接
相信很多人都做过这个问题,用递归写也是非常简短的:

public class t2 {
    public static void main(String[] args) {
        System.out.println(f(61));
    }

    private static int f(int n) {
        if(n<0) return 0;
        if(n==0) return 1;
        int ans = 0;
        ans+=f(n-1);
        ans+=f(n-2);
        ans+=f(n-3);
        return ans;
    }
}

在这里插入图片描述

这样写虽然是正确的,但是在二个阶梯之前,每个阶梯都有三个选择,如果是要爬61个阶梯的话,那么要做3的61次方次的运算,这个时间复杂度是非常高的。

所以我们可以就考虑使用动态规划。

动态规划的基本思想:
将待求解的问题分解成若干个相互联系的子问题,先求解子问题,然后从这些子问题的解得到原问题的解;对于重复出现的子问题,只在第一次遇到的时候对它进行求解,并把答案保存起来,让以后再次遇到时直接引用答案,不必重新求解。
这个黄色部分的话很重要,我们可以观察上面那个图,对于两个阶梯的解,我们求了好多次,这是非常浪费时间和内存的。

package cours;

import java.util.Scanner;

public class t3 {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        int [] arr = new int[n+1];
        arr[1] = 1;
        arr[2] = 2;
        arr[3] = 4;
        for (int i = 4; i < arr.length; i++) {
            arr[i] = arr[i-1]+arr[i-2]+arr[i-3];
        }
        System.out.println(arr[arr.length-1]);
    }
}

这个解法就很好的利用了不用对子问题重复求解的思想,直接将每一层楼梯的方式储存在数组中。
还有一种动态规划是利用记忆性递归来求解。
题目链接
这个链接是我以前写的该题解析。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值