一、步骤
- 把问题转化为规模缩小了的同类问题的子问题
- 有明确的不需要继续进行递归的条件(base case)
- 有当得到了子问题的结果之后的决策过程
- 不记录每一个子问题的解
二、实例
2.1 汉诺塔问题
打印n层汉诺塔从左边移动到最右边的全部过程
2.1.1 思路
不考虑左中右的情况。只考虑需要从源位置from移动到目标位置to,中间暂存的位置是help。那么问题就可以抽象为:
- 1~i-1:from - > help
- i :from - > to
- 1~i-1:help - > to
2.1.2 代码解析
public class Hanoi {
public static void hanoi(int n) {
if (n <= 1) return;
// n层汉诺塔,起始位置圆盘全放在left,目标是全部移动到right
func(n,"left", "mid", "right");
}
public static void func(int i, String from, String help, String to) {
// 当i等于1,说明达到递归终止条件,只剩一个最后圆盘,将它从from移动到to即可
if (i == 1){
System.out.println("Move 1 from " + from + " to " + to);
} else {
// 递归过程,和上述思路完全一致,中间i移动的过程打印
func(i-1, from, help, to);
System.out.println("Move " + i + " from " + from + " to " + to);
func(i-1, help, to, from);
}
}
public static void main(String[] args) {
// 3层汉诺塔的打印示例
int n = 3;
hanoi(n);
}
}
2.2 打印字符串的全部子序列
打印一个字符串的全部子序列,包括空字符串
2.2.1 思路
对于字符串中的每个字符,都有两个选择:要or不要,最后就能得到所有情况的子序列
2.2.2 代码解析
public class PrintAllSubsquences {
public static void printAllSubsquence(String str) {
char[] chs = str.toCharArray();
process(chs, 0);
}
public static void process(char[] chs, int i) {
if(i == chs.length){
System.out.println(String.valueOf(chs));
return;
}
// 要当前字符接着走的路
process(chs, i+1);
char tmp = chs[i];
chs[i] = 0;
// 不要当前字符接着走的路
process(chs, i+1);
chs[i] = tmp;
}
public static void main(String[] args) {
String test = "abc";
printAllSubsquence(test);
}
}
2.3 全排列
打印一个字符串的全排列
2.3.1 思路
从字符串的起点字符开始,将其后续的任一字符与其交换位置则构成一次排列,然后将其恢复继续处理下一位置。(可以利用visited数组优化递归过程,相当于剪枝,避免重复访问造成重复的全排列)
2.3.2 代码解析
public class PrintAllPermutations {
public static ArrayList<String> Permutation(String str) {
// 结果数组
ArrayList<String> res = new ArrayList<>();
if (str == null || str.length() == 0) return res;
char[] chs = str.toCharArray();
process(chs, 0, res);
return res;
}
public static void process(char[] chs, int i, ArrayList<String> res) {
// 如果i等于chs的长度,说明完成了一次全排列,将其加入res中
if(i == chs.length){
res.add(String.valueOf(chs));
}
// visited存储当前位置是否被访问过,防止重复访问造成重复全排列
boolean[] visited = new boolean[26];
// i之前的位置是已经选择的位置,i之后的是可以进行选择的位置
for (int j = i; j < chs.length; j++) {
// 对于i之后的任意位置如果没有被访问过,则可以进行排列
if(!visited[chs[j] - 'a']){
visited[chs[j] - 'a'] = true;
// 排列方式是交换这两个位置完成一次不同排列
swap(chs, i, j);
// 然后继续处理下一位置
process(chs, i+1, res);
// 再将之前交换的位置恢复
swap(chs, i, j);
}
}
}
public static void swap(char[] chs, int i, int j) {
char tmp = chs[i];
chs[i] = chs[j];
chs[j] = tmp;
}
}
2.4 玩纸牌
给定一个整型数组arr,代表数值不同的纸牌排成一条线。玩家A和玩家B依次拿走每张纸 牌,规定玩家A先拿,玩家B后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家A 和玩家B都绝顶聪明。请返回最后获胜者的分数。
【举例】
arr=[1,2,100,4]
开始时,玩家A只能拿走1或4。如果开始时玩家A拿走1,则排列变为[2,100,4],接下来 玩家 B可以拿走2或4,然后继续轮到玩家A...
如果开始时玩家A拿走4,则排列变为[1,2,100],接下来玩家B可以拿走1或100,然后继 续轮到玩家A...
玩家A作为绝顶聪明的人不会先拿4,因为拿4之后,玩家B将拿走100。所以玩家A会先拿1, 让排列变为[2,100,4],接下来玩家B不管怎么选,100都会被玩家 A拿走。玩家A会获胜, 分数为101。所以返回101。
arr=[1,100,2]
开始时,玩家A不管拿1还是2,玩家B作为绝顶聪明的人,都会把100拿走。玩家B会获胜, 分数为100。所以返回100。
2.4.1 思路
主函数返回先手拿牌和后手拿牌中分数高的那个值
- 作为先手拿牌,他有两种选择:
- (选择左边+下一次作为后手拿牌)或者(选择右边+下一次作为后手拿牌)中的更大值
- 当拿到最后一张牌,即L等于R的时候,应该先手拿牌,返回arr[L]作为终止递归的条件
- 作为后手拿牌,也有两种选择:
- 如果别人拿走L,那作为先手在L+1和R上进行选择。如果别人拿走R,则作为先手在L和R-1上进行选择,取这两者中的更小值(对方会决定对我最不利的)
- 当拿到最后一张牌,即L等于R的时候,后手不拿牌,返回0作为终止递归的条件。
2.4.2 代码解析
public class CardsInLine {
// 暴力递归
public static int win1(int[] arr) {
if(arr == null || arr.length == 0){
return 0;
}
return Math.max(f(arr, 0, arr.length-1), s(arr, 0, arr.length-1));
}
public static int f(int[] arr, int i, int j) {
if(i == j){
return arr[i];
}
return Math.max(arr[i] + s(arr, i+1, j), arr[j] + s(arr, i, j-1));
}
public static int s(int[] arr, int i, int j) {
if(i == j) {
return 0;
}
return Math.min(f(arr, i+1, j), f(arr, i, j-1));
}
// 动态优化版本
public static int win2(int[] arr) {
if(arr == null || arr.length == 0) {
return 0;
}
int[][] f = new int[arr.length][arr.length];
int[][] s = new int[arr.length][arr.length];
for (int j = 0; j < arr.length; j++) {
f[j][j] = arr[j];
for (int i = j-1; i >= 0; i--) {
f[i][j] = Math.max(arr[i] + s[i+1][j], arr[j] + s[i][j-1]);
s[i][j] = Math.min(f[i+1][j], f[i][j-1]);
}
}
return Math.max(f(arr, 0, arr.length-1), s(arr, 0, arr.length-1));
}
public static void main(String[] args) {
int[] arr = { 1, 2, 100, 4};
System.out.println(win1(arr));
System.out.println(win2(arr));
}
}
2.5 逆序栈
给定一个栈,请逆序这个栈,不能申请额外的数据结构,只能使用递归函数。
2.5.1 思路
首先设计一个递归函数f,f函数可以实现将栈中最底部元素弹出,其余元素覆盖当前栈的功能。
然后设计逆序栈的函数,通过调用f函数进行递归,最后可以实现栈的逆序。
2.5.2 代码解析
public class ReverseStackUsingRecursive {
public static void reverse(Stack<Integer> stack) {
if(stack.isEmpty()) return;
else{
int i = getAndRemoveLastElement(stack);
reverse(stack);
stack.push(i);
}
}
public static int getAndRemoveLastElement(Stack<Integer> stack) {
int result = stack.pop();
if(stack.isEmpty()){
return result;
} else{
int last = getAndRemoveLastElement(stack);
stack.push(result);
return last;
}
}
public static void main(String[] args) {
Stack<Integer> test = new Stack<Integer>();
test.push(1);
test.push(2);
test.push(3);
test.push(4);
test.push(5);
reverse(test);
while (!test.isEmpty()) {
System.out.println(test.pop());
}
}
}
2.6 数字字符串转化为字母字符串
规定1和A对应、2和B对应、3和C对应...
那么一个数字字符串比如"111",就可以转化为"AAA"、"KA"和"AK"。
给定一个只有数字字符组成的字符串str,返回有多少种转化结果。
2.6.1 思路
假设0到i-1的位置上已经确定,从i位置开始做转化
- 如果i位置上为‘0‘,返回0,因为0没办法转化为字母
- 如果i位置上为’3‘ ~ ’9‘,因为字母只有26个,只能将其转化为对应字母,接着尝试下一个位置。
- 如果i位置上为’1‘ ,有两种选择:
- i自己作为单独部分,接着尝试i+1~最后
- i和i+1作为单独部分,接着尝试i+2~最后
- 如果i位置上为’2‘ ,有两种选择:
- i自己作为单独部分,接着尝试i+1~最后
- 如果i+1的值为’0‘ ~ ’6’,可以将i和i+1作为单独部分,接着尝试i+2~最后
2.6.2 代码解析
public class Code06_ConvertToLetterString {
public static int number(String str) {
if (str == null || str.length() == 0) {
return 0;
}
return process(str.toCharArray(), 0);
}
public static int process(char[] chs, int i) {
if (i == chs.length) return 1;
if (chs[i] == '0') return 0;
if (chs[i] == '1'){
int res = process(chs, i+1);
if (i + 1 < chs.length){
res += process(chs, i+2);
}
return res;
}
if (chs[i] == '2'){
int res = process(chs, i+1);
if (i + 1 < chs.length && chs[i+1] >= '0' && chs[i+1] <= '6'){
res += process(chs, i+2);
}
return res;
} else{
return process(chs, i+1);
}
}
public static void main(String[] args) {
System.out.println(number("11111"));
}
}
2.7 装物品
给定两个长度都为N的数组weights和values,weights[i]和values[i]分别代表 i号物品的重量和价值。给定一个正数bag,表示一个载重bag的袋子,你装的物 品不能超过这个重量。返回你能装下最多的价值是多少?
2.7.1 思路
从weights数组中依次拿出一个物品,看bag剩余空间是否能放这个物品,如果能放则有放与不放两种选择。选择结束之后选过的位置不再变化,继续对下一个物品进行选择。
2.7.2 代码解析
public class Code07_Knapsack {
public static int maxValue1(int[] weights, int[] values, int bag) {
return process1(weights, values, 0, bag);
}
/**
* 暴力递归
* @param weights 重量数组
* @param values 价值数组
* @param i:i之前的是已经确定的,在i之后进行选择
* @param bag:剩余空间
* @return
*/
public static int process1(int[] weights, int[] values, int i, int bag) {
// 如果i来到了最后,则没有选择的了,返回0
if (i == weights.length){
return 0;
}
// 如果剩余空间大于等于放下的物品,有放与不放两种选择,返回其中更大的那个
if (bag >= weights[i]){
return Math.max(process1(weights, values, i+1, bag),
values[i] + process1(weights, values, i+1, bag-weights[i]));
}
return 0;
}
// 动态规划版本
public static int maxValue2(int[] c, int[] p, int bag) {
int[][] dp = new int[c.length+1][bag+1];
for(int i = c.length-1; i >= 0 ; i--){
for(int j = bag; j >= 0; j--){
dp[i][j] = dp[i+1][j];
if(j + c[i] <= bag){
dp[i][j] = Math.max(dp[i][j], p[i] + dp[i+1][j+c[i]]);
}
}
}
return dp[0][0];
}
public static void main(String[] args) {
int[] weights = { 3, 2, 4, 7 };
int[] values = { 5, 6, 3, 19 };
int bag = 11;
System.out.println(maxValue1(weights, values, bag));
System.out.println(maxValue2(weights, values, bag));
}
}
有帮助到你的点赞、收藏和关注一下吧
需要更多教程,微信扫码即可
👆👆👆
别忘了扫码领资料哦【高清Java学习路线图】
和【全套学习视频及配套资料】