一、递归
题目1:求 n! 的结果
题目2:汉诺塔问题
题目3:打印一个字符串的全部子序列,包括空字符串
题目4:打印一个字符串的全部排列
题目5:母牛每年生一只母牛,新出生的母牛成长三年后也能每年生一只母牛,假设不会死。求N年后,母牛的数量
二、动态规划
动态规划的特点
如何把暴力递归套路变为动态规划
题目1:矩阵最小路径和
题目2:(背包问题)从数组任意选择数字,能不能累加得到 aim
递归其实就是不断的尝试,不知道明确的计算方式,但是明白怎么去试。
用递归去求解时:很明显求解 n! 其实就是求解 (n - 1)! 的问题,即它的子问题.....
package com.offer.foundation.class6;
/**
* @author pengcheng
* @date 2019/3/30 - 21:27
* @content: n! 问题
*/
public class Factorial {
// 非递归版本
public long getFactorial1(int n){
long res = 1L;
for (int i = 1; i <= n; i++) {
res *= i;
}
return res;
}
// 递归版本
public long getFactorial2(int n){
if(n == 1){
return 1L;
}
return (long) n * getFactorial2(n - 1);
}
// 测试
public static void main(String[] args) {
Factorial factorial = new Factorial();
System.out.println(factorial.getFactorial1(5)); // 120
System.out.println(factorial.getFactorial2(5)); // 120
}
}
题目:在一根柱子上从下往上按照大小顺序摞着 n 片黄金圆盘。把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,任何时候,在小圆盘上都不能放大圆盘,且在三根柱子之间一次只能移动一个圆盘。打印出移动次数最少的全过程。
把尝试的能力写成代码就是递归的过程。
package com.offer.foundation.class6;
/**
* @author pengcheng
* @date 2019/3/30 - 22:18
* @content: 汉诺塔问题
*/
public class Hanoi {
public void hanoi(int n){
if(n > 0){
hanoi(n, "left", "right", "mid");
}
}
/**
* @param n :n个数
* @param from :原位置
* @param help :辅助位置
* @param to : 目标位置
*/
public void hanoi(int n, String from, String to, String help){
if(n == 1){
// 只有一个时,直接移到目标位置即可
System.out.println(n + ":" + from + "->" + to);
return;
}
// 下面是处理这个过程的递归问题,只用考虑当前n问题就行,不用尝试去理解它的子问题
hanoi(n - 1, from, help, to); // 第1步:将n-1个圆盘从原位置移动到辅助位置
System.out.println(n + ":" + from + "->" + to); // 第2步:将第n个圆盘移到目标位置,即打印即可
hanoi(n - 1, help, to, from); // 第3步:将位置上的n-1个元素移到到目标位置
}
}
每个结点 i:有 要 和 不要 两种选择,之后的随意选择要或不要。
- 子序列顺序不能变
输入:
abc
输出:
// 第一个是空串
c
b
bc
a
ac
ab
abc
package com.offer.foundation.class6;
/**
* @author pengcheng
* @date 2019/3/31 - 15:19
* @content: 打印一个字符串的全部子序列,包括空字符串
*/
public class PrintAllSubString {
public void printAllSub(String str){
if(str == null){
return;
}
char[] chars = str.toCharArray();
if(chars.length > 0){
String pre = new String(""); // pre:表示从0到i-1位置上形成的结果
printAllSub(0, pre, chars);
}else{
System.out.println(""); // 输入空字符串也会打印空
}
}
public void printAllSub(int i, String pre, char[] chars){
// 已经到数组最后一个字符了,所有的选择都做完了,该返回了
if(i == chars.length){
System.out.println(pre);
return;
}
// 如果没有到最后一个字符,那么当前字符两种选择:选择要或者选择不要
printAllSub(i + 1, pre, chars); // 不要当前字符
printAllSub(i + 1, pre + String.valueOf(chars[i]), chars); // 要当前字符
}
// 测试
public static void main(String[] args) {
PrintAllSubString p = new PrintAllSubString();
String str = "abc";
p.printAllSub(str);
}
}
4.1 打印一个字符串的全部排列【每个结点i:有i~n-1种选择,之后的随意排序】
package com.offer.foundation.class6;
/**
* @author pengcheng
* @date 2019/4/4 - 16:00
* @content:
*/
public class PrintAllSort {
public static void printAllSort(String string){
if(string == null){
return;
}
char[] chars = string.toCharArray();
if(chars.length > 0){
func(0, chars);
}
}
// 对i及i以后的字符进行全排序
public static void func(int i, char[] chars){
if(i == chars.length){
System.out.println(String.valueOf(chars));
}
for(int j = i; j < chars.length; j++){
swap(i, j, chars); // 第i个位置有i~n-1这些选择
func(i + 1, chars); // 搞第i+1的位置
swap(i, j, chars);
}
}
public static void swap(int i, int j, char[] chars){
char temp = chars[i];
chars[i] = chars[j];
chars[j] = temp;
}
// 测试
public static void main(String[] args) {
printAllSort("abc");
}
}
4.2 进阶:打印一个字符串的全部排列,要求不要出现重复的排列
什么是不重复的字符串全排列,如果是普通字符串全排列,那么
输入:
acc
输出:【即认为后面两个c是不一样的,4.1的做法】
acc
acc
cac
cca
cca
cac
要求写出的去重的,也就是会输出:acc
cac
cca
【即认为后面两个c是一样的】
package com.offer.foundation.class6;
import java.util.HashSet;
/**
* @author pengcheng
* @date 2019/4/4 - 16:00
* @content:
*/
public class PrintAllSort {
public static void printAllSort(String string){
if(string == null){
return;
}
char[] chars = string.toCharArray();
if(chars.length > 0){
func2(0, chars);
}
}
// 对i及i以后的字符进行全排序
public static void func2(int i, char[] chars){
if(i == chars.length){
System.out.println(String.valueOf(chars));
}
// 用于保证每次交换的字符不存在重复字符
HashSet set = new HashSet<>();
for(int j = i; j < chars.length; j++){
// 只有之前没有交换过这个字符才会交换
if(!set.contains(chars[j])) {
set.add(chars[j]);
swap(i, j, chars); // 第i个位置有i~n-1这些选择
func2(i + 1, chars); // 搞第i+1的位置
swap(i, j, chars);
}
}
}
public static void swap(int i, int j, char[] chars){
char temp = chars[i];
chars[i] = chars[j];
chars[j] = temp;
}
// 测试
public static void main(String[] args) {
printAllSort("acc");
}
}
第1年 | 第2年 | 第3年 | 第4年 | 第5年 | 第6年 |
---|---|---|---|---|---|
1 | 2 | 3 | 4 | 6 | 9 |
A | A | A | A | A | A |
B(A生) | B(A生) | B(A生) | B(A生) | B(A生) | |
C(A生) | C(A生) | C(A生) | C(A生) | ||
D(A生) | D(A生) | D(A生) | |||
E(A生) | E(A生) | ||||
F(B生) | F(B生) | ||||
G(A生) | |||||
H(B生) | |||||
I(C生) |
package com.offer.foundation.class6;
/**
* @author pengcheng
* @date 2019/3/31 - 17:09
* @content: 母牛数量问题
*/
public class CowNum {
// 求第n年的牛的数量
public static int cowNum(int n){
if(n == 1){
return 1;
}
if(n == 2){
return 2;
}
if(n == 3){
return 3;
}
return cowNum(n - 1) + cowNum(n - 3);
}
// 测试
public static void main(String[] args) {
int num = cowNum(5);
System.out.println(num);
}
}
进阶:如果每只母牛只能活10年,求N年后,母牛的数量。
public static int cowNum2(int n){
if(n <= 3){
return n;
}else if(n <= 10){
return cowNum2(n - 1) + cowNum2(n - 3);
}else{
return cowNum2(n - 1) + cowNum2(n - 3) + cowNum2(n - 10);
}
}
动态规划是从basecase往上推得到 n ,而递归是从 n 推到basecase再一个一个的返回来得到 n 的结果)
给你一个二维数组,二维数组中的每个数都是正数,要求从左上角走到右下角,每一步只能向右或者向下。沿途经过的数字要累加起来。返回最小的路径和。
1、递归版本
package com.offer.foundation.class6;
/**
* @author pengcheng
* @date 2019/4/4 - 16:41
* @content: 矩阵最小路径和问题
*/
public class MinPath {
public static int minPath(int[][] matrix){
if(matrix == null || matrix.length == 0 || matrix[0] == null || matrix[0].length == 0){
return 0;
}
// 从左上角走到右下角
return walk(matrix, 0, 0);
}
// 从[i,j]位置走到右下角
public static int walk(int[][] matrix, int i, int j){
if(i == matrix.length - 1 && j == matrix[0].length - 1){
// [i,j]位置已经在右下角了
return 0;
}
if(i == matrix.length - 1){
// [i,j]在矩阵的最后一行,所以只能往右走了
return matrix[i][j] + walk(matrix, i, j + 1);
}
if(j == matrix[0].length - 1){
// [i,j]在矩阵的最后一列,所以只能往下走了
return matrix[i][j] + walk(matrix, i + 1, j);
}
int right = walk(matrix, i, j + 1);
int down = walk(matrix, i + 1, j);
return matrix[i][j] + Math.min(right,down);
}
}
2、动态规划版本
递归版本虽然简单,但是时间复杂度过高,显然是不行的。通过分析发现,在递归过程中,会有很多重复的计算,如下图所示:
在计算(1,0)位置的右元素和计算(0,1)位置的下元素时,发生了重复计算:都是计算(1,1)位置到右下角的最小距离和。这里只是分析了两步,如果继续分析,会出现很多类似的重复计算过程。
1、无后效性:无论(1,1)位置是从(1,0)位置来的还是(0,1)位置来的,都不影响(1,1)位置到右下角的最小距离的结果,这就叫做无后效性,反之则是有后效性。
2、无后效性一定可以改成递归版本。
3、汉诺塔问题:每步需要打印出轨迹,所以是有后效性的。
4、八皇后问题:前一步的选择会影响后一步的结果,是有后效性的。
那么我们是不是可以利用缓存将每次的计算结果存储起来,下一次再碰到相同元素计算的时候先去缓存中查找看是否已经计算过了,如果存在则直接使用,在没有计算过的时候再去计算,并将结果存储到缓存中。很明显这样的缓存可以用map实现,元素对应key,结果对应value。
改递归思路:
利用basecase(即:i == matrix.length - 1 && j == matrix[0].length - 1)可以直接得出图中状态表右下角的位置为6,然后再由6推出最后一行和最右一列的状态值,然后又可以利用刚才推出的值进行新的一轮推到.....最终将整个表的每个位置都填上其对应的状态值。如上图所示:左上角位置状态值为17,即代表从左上角到右下角位置最短路径值为:17。
这个过程就盖楼一样,从地基开始,上层依赖下层。下层盖好了,上层就可以盖了。
package com.offer.foundation.class6;
import java.util.HashMap;
/**
* @author pengcheng
* @date 2019/4/4 - 16:41
* @content: 矩阵最小路径和问题
*/
public class MinPath {
// 动态规划版本
public static int walkDynamic(int[][] matrix){
if(matrix == null || matrix.length == 0 || matrix[0] == null || matrix[0].length == 0){
return 0;
}
int lastRow = matrix.length - 1;
int lastCol = matrix[0].length - 1;
int[][] dp = new int[lastRow][lastCol]; // 状态表
dp[lastRow][lastCol] = matrix[lastRow][lastCol]; // basecase:右下角到右下角的距离为其本身大小
// 填充最后一行其他位置处的状态值
for(int i = lastRow, j = lastCol - 1; j >= 0; j--){
// 左边位置的值等于右边位置值加上它自身的值
dp[i][j] = matrix[i][j] + dp[i][j + 1];
}
// 填充最后一列其他位置处的状态值
for(int j = lastCol, i = lastRow - 1; i >= 0; i--){
// 上面的位置等于下面的位置值加上它本身的值
dp[i][j] = matrix[i][j] + dp[i + 1][j];
}
// 填充一般位置(除最后一行和最右一列的位置)
for(int i = lastRow - 1; i >=0; i--){
for(int j = lastCol - 1; j >= 0; j--){
// 一般位置:当前位置值 + min(下面位置值,右面位置值)
dp[i][j] = matrix[i][j] + Math.min(dp[i + 1][j],dp[i][j + 1]);
}
}
return dp[0][0]; // 返回目标值
}
}
给你一个数组 arr,和一个整数 aim。如果可以任意选择 arr 中的数字,能不能累加得到 aim,返回 true 或者 false。
1、递归版本
如上图所示:数组 arr = {3, 2, 5} ,aim = 7:
f(0, 0):代表0位置处状态值为0的点;
f(2, 5):代表2位置处状态值为5的点。
只要有叶节点的值等于 aim 的值,则会返回 true。
package com.offer.foundation.class6;
/**
* @author pengcheng
* @date 2019/4/4 - 19:27
* @content:
*/
public class SumToAim {
public static boolean IsSumToAim(int[] arr, int aim){
if(arr == null){
return false;
}
return process(arr, 0, 0, aim);
}
// pre:是 0 ~ (i - 1)随意相加产生的结果
// 用于判断pre+i及其后面的数字随意相加,是否能够得到aim
public static boolean process(int[] arr, int i, int pre, int aim){
if(i == arr.length){
return pre == aim;
}
// 位置i有两种选择:要或不要,有一个等于aim,即返回true
return process(arr, i + 1, pre, aim) || process(arr, i + 1, pre + arr[i], aim);
}
}
2、动态规划版本
状态表如上图所示,横坐标为 m 的值,纵坐标为 i 的值。从 basecase 可以看出最后一行的状态值是可以确定的,所以从最后一行往上推导,一直推导到左上角的位置处,如果为 True,则返回 True(图中空白处都为false)。
怎么通过下面一行的状态值得出上面一行的状态值呢?看递归的代码:
process(arr, i + 1, pre, aim) || process(arr, i + 1, pre + arr[i], aim)
因此:
package com.offer.foundation.class6;
/**
* @author pengcheng
* @date 2019/4/4 - 19:27
* @content:
*/
public class SumToAim {
// 递归版本
public static boolean isSumToAim2(int[] arr, int aim){
if(arr == null || arr.length == 0){
return false;
}
// 状态表:需要注意到底需要几行
boolean[][] dp = new boolean[arr.length + 1][aim + 1];
// 填好最后一行:i为横坐标,pre为纵坐标
for(int i = arr.length, sum = 0; sum <= aim; sum++){
if(sum == aim){
dp[i][sum] = true; // 目标值处设置为true
}else{
dp[i][sum] = false;
}
}
// 按照递归填好状态表中的每一个位置:从下一行推导出上一行的状态值
for(int i = arr.length - 1; i >= 0; i--){
for(int sum = aim; sum >= 0; sum--){
if(sum + arr[i] > aim){
dp[i][sum] = dp[i + 1][sum];
}else{
// dp[i][sum]值为true的两种情况:正下方值为true || dp[i+1][sum+arr[i]]的值为true,有一个为ture就行
dp[i][sum] = dp[i + 1][sum] || dp[i + 1][sum + arr[i]];
}
}
}
return dp[0][0];
}
}