首先给大家分享一句话,
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]);
}
}
这个解法就很好的利用了不用对子问题重复求解的思想,直接将每一层楼梯的方式储存在数组中。
还有一种动态规划是利用记忆性递归来求解。
题目链接
这个链接是我以前写的该题解析。