核心问题是穷举
因为要求最值,肯定要把所有可行的答案穷举出来,然后在其中找最值。
递归的问题,都是树的问题,就是树的遍历。
特点:
- 穷举,存在大量的「重叠子问题」
- 具备「最优子结构」
- 列出正确的「状态转移方程」,穷举时问题可能变化,要确定好边界值
明确 base case -> 明确「状态」-> 明确「选择」 -> 定义 dp 数组/函数的含义。
步骤:
- 画树,找出重复子问题
- 写出子问题基本框架
- 找出边界值,最大最小值等
- 完善代码至可执行状态
- 优化代码,添加备忘录记录下重复处理的子问题答案,降低时间复杂度。
斐波那契数列
1.暴力递归
递归问题都是树的遍历问题,先把树画出来。
由图可得出,递归最基础的公式:
再加上边界值,得出:
private static int test1(int n) {if (n == 1 || n == 2) {return 1;}return test1(n - 1) + test1(n - 2);}
空间复杂度:
时间复杂度:,二叉树节点总数,指数级别。
2.带备忘录的递归解法

虽然暴力解法简单快捷,但是存在大量重复运算。
解法:把已计算过的数值记录起来,如哈希表、数组等。
2.1备忘录-哈希表
/*** 2.备忘录 把已计算的值记录下来 (自顶向下)*/private static int test2(int n) {Map<Integer, Integer> map = new HashMap<>();return test22(map, n);}private static int test22(Map<Integer, Integer> map, int n) {if (n == 1 || n == 2) {return 1;}if (map.containsKey(n)) {return map.get(n);}int i = test22(map, n - 1) + test22(map, n - 2);map.put(n, i);return i;}
递归过程中哈希表做了什么:
哈希表记录把二叉树指数级别的计算转化成线性级别的计算。
时间复杂度:
空间复杂度:
2.2备忘录-数组

本质上和用哈希表把二叉树指数级别转化成线性级别是一样的,只不过转换后的链表顺序相反。
/*** 3.备忘录 把已计算的值记录下来 (自底向上)--数组*/private static int test3(int n) {if (n == 1 || n == 2) {return 1;}int[] a = new int[n + 1];a[1] = 1;a[2] = 1;for (int i = 3; i <= n; i++) {a[i] = a[i - 1] + a[i - 2];}return a[n];}
时间复杂度:
空间复杂度:
2.2优化备忘录
备忘录-数组我们发现,其实每次遍历仅仅需要n-1和n-2的值,n-3前的值已经是无用的,且遍历是有序递增。
/*** 4.备忘录 把已计算的值记录下来 (自底向上)-- 只记录n-1和n-2有用的值*/private static int test4(int n) {if (n == 1 || n == 2) {return 1;}int pre1 = 1;int pre2 = 1;int sum = 0;for (int i = 3; i <= n; i++) {sum = pre1 + pre2;pre1 = pre2;pre2 = sum;}return sum;}
时间复杂度:
空间复杂度:, 只用到了pre1,pre2,sum
零钱兑换
斐波那契数列可能较为简单,还不需要完整的解题思路,零钱兑换这道题则可以很好地运用到一开始讲到的思路。
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。 你可以认为每种硬币的数量是无限的。
示例: 输入:coins =
[1, 2, 5], amount =11输出:3解释:11 = 5 + 5 + 1
1.暴力递归
第一步:画树,找出子问题
核心问题:金额amount,至少需要n个币
伪代码:
//伪代码://step1:找出子问题的基本框架int step1(int[] coins, int amount) {int min = Integer.MAX_VALUE;//在同一个子问题中,选取最优解(最小值)for (int coin : coins) {int res = step1(coins, amount - coin);min = MIN(res,min)}//+1为需要币数量+1return min+1;}
第二步:找出边界值(base case)
这道问题的 base case 就是金额amount的值为0时,需要0个币,小于0时无解返回-1
//找出边界值,并完善代码int step2(int[] coins, int amount) {if (amount < 0) {return -1;}//base caseif (amount == 0) {return 0;}int min = Integer.MAX_VALUE;//在同一个子问题中,选取最优解(最小值)for (int coin : coins) {int res = step1(coins, amount - coin);//边界值找出后,对判断进行修改if(res>=0 && res<min){min = res;}}//再处理边界问题,可能子问题一直无解return (min == Integer.MAX_VALUE) ? -1 : min + 1;}
以上完整的暴力解法已经出来,再加上备忘录降低时间复杂度。
2.递归-备忘录
/*** 步骤三:添加备忘录降低时间复杂度*/private static int step3(int[] coins, int amount) {if (amount < 1) {return 0;}return step33(coins, amount, new int[amount]);}private static int step33(int[] coins, int amount, int[] memos) {if (amount < 0) {return -1;}if (amount == 0) {return 0;}//备忘录-有则直接返回if (memos[amount-1] !=0){return memos[amount-1];}int min = Integer.MAX_VALUE;for (int coin : coins) {int res = step33(coins, amount - coin, memos);if (res < min && res >= 0) {min = res;}}min = (min == Integer.MAX_VALUE) ? -1 : min + 1;//插入备忘录memos[amount-1] = min;return min;}
3.递归-备忘录-自底向上
数组大小为金额大小+1,从金额0开始算
0, 3, 4为5-5, 5-2, 5-1
private static int step44(int[] coins, int amount) {if (amount < 0) {return -1;}if (amount == 0) {return 0;}//初始化备忘录,大小amount + 1int[] memos = new int[amount + 1];//初始化数组各个值为amount + 1Arrays.fill(memos, amount + 1);//第一个边界值0memos[0] = 0;for (int i = 0; i < amount + 1; i++) {for (int coin : coins) {if (i - coin < 0) {//子问题无解,跳过continue;}memos[i] = Math.min(memos[i], 1 + memos[i - coin]);}}return (memos[amount] == amount + 1) ? -1 : memos[amount];}
