大拆小拆小
base-case:不需要进一步划分
汉诺塔问题
问题背景
汉诺塔问题是一个经典的问题。汉诺塔(Hanoi Tower),又称河内塔,源于印度一个古老传说。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,任何时候,在小圆盘上都不能放大圆盘,且在三根柱子之间一次只能移动一个圆盘。问应该如何操作?
汉诺塔——经典递归问题(c语言实现)_汉诺塔递归算法_lucas_dd的博客-CSDN博客
举个栗子:三层汉诺塔问题
A | B | C |
1 2 3 | 0 | 0 |
2 3 | 0 | 1 |
3 | 2 | 1 |
3 | 1 2 | 0 |
0 | 1 2 | 3 |
1 | 2 | 3 |
1 | 0 | 2 3 |
0 | 0 | 1 2 3 |
实现解析
三个杆:出发的杆 from;到达的杆 to;另一个杆 other
对于每一层的汉诺塔都有这样的步骤:
1、将 1 ~ i-1 层的汉诺塔从from移动到other上
2、将 i 层的汉诺塔从from移动到other上
3、将 1 ~ i-1 层的汉诺塔从other移动到to上
由于在递归的过程中,from、to、other的三个值会被改变,因此输出的from到to不一定就是左到右
关于Move i from to to这一句代码的写法,只需要参考最外层;因为最外层的汉诺塔只会移动一次,那就是from到to(最外层的from和to)
由此可知,向内递归,我们最终的目的就是将每层的汉诺塔都从from移动到to,因此是这一句代码
怎么保证大的汉诺塔在小的汉诺塔之下?
是由递归的子过程决定的。每一步,子问题都满足自己的底(第i层)满足规则。也就是对每一层(第i层)的汉诺塔来说,都满足从from到 to
递归:保证局部的正确即整体的正确,主打的一个尝试
代码
package recursion;
public class HanoiTower {
public static void main(String[] args) {
int n = 3;
hanoi(n);
}
public static void hanoi(int n) {
if (n > 0) {
process(n, "左", "右", "中");
}
}
public static void process(int i, String from, String to, String other) {
//base-case
if (i == 1) {
System.out.println("Move 1 " + from + " to " + to);
} else {
process(i - 1, from, other, to);
System.out.println("Move " + i + " " + from + " to " + to);
process(i - 1, other, to, from);
}
}
}
打印一个字符串的全部子序列
得到所有的子序列 == 求全部的子集
实现解析
从左往右每个字符在要和不要之间做决策
方法一
为什么不直接使用res,而是复制一份res?
递归找回来的时候,还得不断增减res,比较麻烦
package recursion;
import java.util.List;
public class PrintsAllSubsequences {
//res记录之前打印的字符,Character - char类型的包装类
public static void process(char[] str, int i, List<Character> res) {
//base-case
if (i == str.length){
printStr(str);
}
//要当前字符的路
List<Character> resKeep = copyRes(res);//复制一份res
resKeep.add(str[i]);//加上当前字符
process(str, i+1, resKeep);//向下走
//不要当前字符的路
List<Character> resNoKeep = copyRes(res);//复制一份res,并且不加当前字符
process(str,i+1,resNoKeep);//向下走
}
public static void printStr(char[] str){
//print
}
public static List<Character> copyRes(List<Character> res){
return null;
}
}
方法二
实现了str空间的复用,比方法一要节省空间,但是时间复杂度是一样的
递归的过程中,外层的变量一直存在,只是被压到栈中。当递归出来的时候,栈中的变量就被还原出来
package recursion;
public class PrintsAllSubsequencesOptimize {
public static void process(char[] str, int i) {
if (i == str.length) {
System.out.println(String.valueOf(str));
return;
}
//要当前字符的路
process(str, i + 1);
char temp = str[i];
//不要当前字符的路
str[i] = 0;//char类型放入零,ASCII码没有输出值
process(str, i + 1);
str[i] = temp;//值放回来,防止回溯的时候丢失str[i]的值
}
}
打印一个字符串的全部排列
对一个字符串abc进行全排列
abc bac cab
acb bca cba
固定第一个字符,尝试后面 i-1 个字符串的全排列
base-case:个数为1的时候
字符串 i 位置之后的所有字符,都可以在 i 位置上进行尝试
0~ i-1范围上是之前所作的选择,是已确定的定死的
其中对于第一个字符,有n种可能性
对于第二个字符,有n-1种可能性
对于第三个字符,有n-2种可能性
......
package recursion;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
public class PrintFullArrangement {
@Test
public static void printFullArrangement(String[] args) {
String str = "abc";
char[] chs = str.toCharArray();
ArrayList<String> res = new ArrayList<>();
res = process(chs, 0, res);
for (String s : res) {
System.out.println(s);
}
}
//i定位当前位置,res表示返回结果
public static ArrayList<String> process(char[] chs, int i, ArrayList<String> res) {
//base-case,来到末尾位置
if (i == chs.length) {
res.add(Arrays.toString(chs));
}
//递归
for (int j = i; j < chs.length; j++) {
swap(chs, i, j);//后续所有的字符都可以来到i位置
process(chs, i + 1, res);//走后续
swap(chs, i, j);//递归回来还原回原来的字符
}
return res;
}
public static void swap(char[] chs, int i, int j) {
char c = ' ';
c = chs[j];
chs[j] = chs[i];
chs[i] = c;
}
}
改进
去重,有两种方式
1、所有的数据添加到res集合之后再去重
2、分支限界/剪枝,通过boolean[]判定是否已经尝试过,在一边走递归的路一边实现去重
指标未优化,但是常数项优化
//i定位当前位置,res表示返回结果
public static ArrayList<String> process(char[] chs, int i, ArrayList<String> res) {
//base-case,来到末尾位置
if (i == chs.length) {
res.add(Arrays.toString(chs));
}
//对应a~z的26个小写字母,默认为false
boolean[] visit = new boolean[26];
//递归
for (int j = i; j < chs.length; j++) {
if (!visit[chs[j] - 'a']) {
//true代表这个字符串已经尝试过,之后不再尝试
visit[chs[j] - 'a'] = true;
swap(chs, i, j);//后续所有的字符都可以来到i位置
process(chs, i + 1, res);//走后续
swap(chs, i, j);//递归回来还原回原来的字符
}
}
return res;
}
排成一条线的纸牌博弈问题
在L到R上的范围上尝试,返回L到R范围上最大分数,范围之外的所有数都是已经尝试过的
先手函数first(arr, L, R)
如果只剩下一个数,L==R,并且我是先手,我拿到这个数,返回arr[L]
如果不只是剩下一个数,分为以下两种情况:
对于我自己来说,如果拿L,则会获得arr[L],那么剩下的L+1~R范围对于我自己来说是后手,获得的分数是second(arr, L+1, R)
拿L,获得的分数是arr[L] + second(arr, L+1,R)
对于我自己来说,如果拿R,则会获得arr[R],那么剩下的L~R-1范围对于我自己来说是后手,获得的分数是second(arr, L, R-1)
拿R,获得的分数是arr[R] + second(arr, L, R-1)
作为绝顶聪明的人,一定会取对自己更加有利的方面,即是在arr[L] + second(arr, L+1,R)和arr[R] + second(arr, L, R-1)取较大值
后手函数second(arr, L, R)
如果只剩下一个数,L==R,并且我是后手,那么这个数一定是被对方拿走的,我自己只能拿到0
如果不只是剩下一个数,分为以下两种情况:
别人拿走L,表示别人是后手,那么我就先手了,分数是first(arr, L-1, R)
别人拿走R,并且别人是后手,那么我就先手了,分数是first(arr, L-1, R)
作为绝顶聪明的人,但是我此时的分数是由对方决定的,那么对方肯定会选择对自己有利,对我不利的那一种情况,即是在first(arr, L+1, R)和first(arr, L, R-1)中取较小值
package recursion;
import org.junit.Test;
public class Card {
@Test
public void card() {
int[] arr = {1, 2, 100, 4};
System.out.println(win1(arr));
}
public int win1(int[] arr) {
if (arr == null || arr.length == 0) {
return 0;
}
return Math.max(first(arr, 0, arr.length - 1), second(arr, 0, arr.length - 1));
}
//先手函数
public Integer first(int[] arr, int L, int R) {
//如果只剩下一个数,L==R,并且我是先手,我拿到这个数,返回arr[L]
if (L == R) {
return arr[L];
}
//作为绝顶聪明的人,一定会取对自己更加有利的方面
// 即是在arr[L] + second(arr, L+1,R)和arr[R] + second(arr, L, R-1)取较大值
return Math.max(arr[L] + second(arr, L + 1, R), arr[R] + second(arr, L, R - 1));
}
//后手函数
public Integer second(int[] arr, int L, int R) {
//如果只剩下一个数,L==R,并且我是后手,那么这个数一定是被对方拿走的,我自己只能拿到0
if (L == R) {
return 0;
}
//作为绝顶聪明的人,但是我此时的分数是由对方决定的,那么对方肯定会选择对自己有利,对我不利的那一种情况
// 即是在first(arr, L+1, R)和first(arr, L, R-1)中取较小值
return Math.min(first(arr, L + 1, R), first(arr, L, R - 1));
}
}
递归实现逆序栈
给定一个remove函数,在调用这个函数之后,将栈底的元素取出,栈由左变为右,并且返回3
remove函数的实现
调用remove1(),栈弹出1,记录在r变量之中,last存储下一层弹出的值
调用remove2(),栈弹出2,记录在r变量之中,last存储下一层弹出的值
调用remove3(),栈弹出3,此时栈空了
栈空之后,直接返回result,remove3()返回3,存储到remove2()中的last值中
remove2()拿到last值之后,将result值2压回栈中去
remove2()返回last的值3,存储到remove1()中的last值中
remove1()拿到last值之后,将result值1压回栈中去
remove1()返回last的值3,即整个remove/f函数返回的值为3,并且实现了将栈底的元素取出
public static Integer remove(Stack<Integer> stack){
int result = stack.pop();
//栈空的情况,直接返回result,remove3()返回
if(stack.isEmpty()){
return result;
}
int last = remove(stack);
stack.push(result);//将除了栈底的元素重新压回栈中
//栈非空的情况,返回last,remove1()、remove2()返回
return last;
}
reverse函数的实现
实现栈的反转
调用reverse1(),存储了i=3,调用子过程的reverse2()
调用reverse2(),存储了i=2,调用子过程的reverse3()
调用reverse3(),存储了i=1,调用子过程的reverse4()
调用reverse4(),进来发现栈为空,什么都不做,回到了reverse3()
reverse3()将1压回栈中,回到了reverse2()
reverse2()将2压回栈中,回到了reverse1()
reverse1()将3压回栈中,什么都不返回,结束一整个reverse方法
//不使用额外的空间
public static void reserve2(Stack<Integer> stack) {
if (stack.isEmpty()) {
return;
}
int i = remove(stack);
reserve2(stack);
stack.push(i);
}
递归实现逆序栈代码
package recursion;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Stack;
public class ReverseStack {
@Test
public void reverseStack() {
Stack<Integer> stack = new Stack<>();
stack.push(1);
stack.push(2);
stack.push(3);
// reserve1(stack);
reserve2(stack);
while (!stack.isEmpty()) {
System.out.println(stack.pop());
}
}
//不符合题意,使用了额外的空间
public static void reserve1(Stack<Integer> stack) {
if (stack.isEmpty()) {
return;
}
ArrayList<Integer> arr = new ArrayList<>();
while (!stack.isEmpty()) {
arr.add(remove(stack));
}
for (int i = arr.size() - 1; i >= 0; i--) {
stack.push(arr.get(i));
}
}
//不使用额外的空间
public static void reserve2(Stack<Integer> stack) {
if (stack.isEmpty()) {
return;
}
int i = remove(stack);
reserve2(stack);
stack.push(i);
}
public static Integer remove(Stack<Integer> stack) {
int result = stack.pop();
//栈空的情况,直接返回result,remove3()返回
if (stack.isEmpty()) {
return result;
}
int last = remove(stack);
stack.push(result);//将除了栈底的元素重新压回栈中
//栈非空的情况,返回last,remove1()、remove2()返回
return last;
}
}
数字与字母对应求字符串的转化结果
从左向右尝试
来到i位置,0~i-1位置是确定的,i往后的位置是可以自由转化的
如果i位置是0字符,返回0。因为0不能作为某个数字的第一位,并且0自己也无法对应字母,因此只能作为前面几个数字组成的最后一位
如果i位置是3~9,只能在i位置自己转化,i和i+1位置不能够一起进行转化,因为一共只有26个字母可以对应
如果i位置是1,既可以i位置自己转化,也可以i和i+1位置一起转化
如果i位置是2,需要看i和i+1共同组成的数字是否超过26。
如果超过26,只能i位置自己转化;
如果没有超过26,则既可以自己转化,也可以和后一位一起转化
//i位置之前的字符串是已经完成的,i位置及之后的字符是未完成判断的
public static Integer process(String[] str, Integer i) {
//i来到最后位置,之前做的决定都有效,成为了一种情况
if (i == str.length) {
return 1;
}
//现在i位置是0,说明前面的决定是错误的,所以整体的方法pass,是0种有效的方式
if (str[i].equals("0")) {
return 0;
}
if (str[i].equals("1")) {
int res = process(str, i + 1);//只能i位置单独转化
//注意数组越界问题
if (i + 1 < str.length) {
res += process(str, i + 2);//i和i+1位置一块转化,两种情况都可能成立,所以是相加的
}
return res;
}
if (str[i].equals("2")) {
int res = process(str, i + 1);
//整体在20到26之间并且不越界
if (Integer.parseInt(str[i + 1]) < 7 && Integer.parseInt(str[i + 1]) >= 0 && i + 1 < str.length) {
res += process(str, i + 2);
}
return res;
}
return process(str, i + 1);//3~9
}
背包问题
从左向右尝试
0号货要和不要展开,1号货要和不要展开,2号货要和不要展开......
i之前的货物已经决定,i及其之后的货物未决定,形成最大价值返回
//i位置之前的货物自由选择,形成最大价值返回
public static Integer process1(int[] weights, int[] values, int i, int alreadyWeight, int bag) {
//当前重量不超过bag,需要先判断是否超重
if (alreadyWeight > bag) {
return 0;
}
//此方法返回的是i及其之后位置进行选择的最大价值的,已经来到最后了那么后面就是没有货物了,因此返回0
if (i == weights.length) {
return 0;
}
return Math.max(
process1(weights, values, i + 1, alreadyWeight, bag),//不要i位置的货
values[i] + process1(weights, values, i + 1, alreadyWeight, bag)//要i位置的货
);
}
public static Integer process4(int[] weights, int[] values, int i, int alreadyWeight, int alreadtValue, int bag) {
//需要先判断是否超重
if (alreadyWeight > bag) {
return 0;
}
if (i == weights.length) {
return alreadtValue;
}
return Math.max(
//不要i号货
process4(weights, values, i + 1, alreadyWeight, alreadtValue, bag),
//要i号货
process4(weights, values, i + 1, alreadyWeight + weights[i], alreadtValue + values[i], bag)
);
}