暴力递归
概述
- 暴力递归就是尝试(局部尝试)
- 把问题转化为规模缩小了的同类问题的子问题
- 明确递归结束的条件(base case)
- 得到子问题的结果后有决策过程
- 不记录每一个子问题的解,尝试最重要
题目一:汉诺塔问题
- 思路:
(1)将 1 ~ n - 1 层移动到中间
(2)将 n 层移动到最右边
(3)将 1 ~ n - 1 层移动到最右边
public static void hanoi(int n){
if (n > 0){
move(n, "left", "mid", "right");
}
}
public static void move(int n, String from, String to, String temp){
//移动from:出发地;to:目的地;temp:辅助位置
if (n == 1){
//递归结束条件
System.out.println("Move 1 form " + from + " to " + to);
}
else {
//问题规模的缩小
//移动 n - 1 由from到temp
move(n - 1, from, temp, to);
//移动 n 由from到to
System.out.println("Move " + n + " from " + from + " to " + to);
//移动n - 1 由temp到to
move(n - 1, temp, to, from);
}
}
- 结果演示:
当 n = 3
题目二:字符串的全部子序列问题
- 思路:
尝试从0 到 n位置
(1)将字符串转化为字符数组,每个字符位置分为要和不要
(2)问题从0 到 n的位置,每次增加一个位置
(3)当长度等于 n 结束递归
注意点:一个递归结束后若对字符数组进行了改变,再下一个递归开始前要恢复字符数组
public static void printAllSubsquence(String str){
char[] chs = str.toCharArray();
progress(chs, 0);
}
public static void progress(char[] chs, int index){
//index,在字符数组中目前到达的位置
if (index == chs.length){
//走完整个字符数组
System.out.println(String.valueOf(chs));
return;
}
//直接后移保留index位置,规模的缩小
progress(chs, index + 1);
char temp = chs[index];
//ASCLL中0表示空字符
chs[index] = 0;
//将index出使用空字符代替,规模的缩小
progress(chs, index + 1);
//恢复index位置
chs[index] = temp;
}
- 结果演示
str = “ljk”
题目三:字符串的全排列问题(分支限界)
- 思路:
若字符串的长度为n
尝试从0到n位置
不考虑重复排列时:使用交换的方法
(1)第一个位置可以有N个位置与其交换
(2)第二个位置可以有N - 1个位置与其交换
……
(3)第N - 1个位置有2个位置可以与其交换
(4)第N个位置只有1个位置与其交换(递归结束的条件,交换到第N个位置)
注意点:一个递归结束后若对字符数组进行了改变,再下一个递归开始前要恢复字符数组
//全排列
public static void permutation(String str){
char[] chs = str.toCharArray();
progress(chs, 0);
}
public static void progress(char[] chs, int index){
//index,在字符数组中目前到达的位置
if (index == chs.length){
//走完整个字符数组
System.out.println(String.valueOf(chs));
return;
}
for (int i = index; i < chs.length; i++){
//index位置与其后面的位置进行交换
swap(chs, index, i);
//规模的缩小
progress(chs, index + 1);
//一次递归结束恢复原字符数组
swap(chs, index, i);
}
}
public static void swap(char[] chs, int i, int j){
char temp = chs[i];
chs[i] = chs[j];
chs[j] = temp;
}
- 结果演示
str = “abb”
- 思路:
若字符串的长度为n,字符均为小写字母
考虑重复排列时:出现相同的字母时不进行位置交换
public static void permutation(String str){
char[] chs = str.toCharArray();
progress(chs, 0);
}
public static void progress(char[] chs, int index){
//index,在字符数组中目前到达的位置
if (index == chs.length){
//走完整个字符数组
System.out.println(String.valueOf(chs));
return;
}
//记录26个小写字母是否有相同的
boolean[] visit = new boolean[26]; //默认为false
for (int i = index; i < chs.length; i++) {
if (!visit[chs[i] - 'a']) {
//i位置的元素没有重复,可以与index位置交换
//分支限界的思想,提前结束不可能的分支
visit[chs[i] - 'a'] = true;
//index位置与其后面的位置进行交换
swap(chs, index, i);
//规模的缩小
progress(chs, index + 1);
//一次递归结束恢复原字符数组
swap(chs, index, i);
}
}
}
public static void swap(char[] chs, int i, int j){
char temp = chs[i];
chs[i] = chs[j];
chs[j] = temp;
}
- 结果演示:
str = “abb"
题目四:拿纸牌比最大问题
- 思路:
(1)分为两种拿牌方式,先手和后手,若范围为left ~ right
(2)先手分为左右,若拿left,值就是就是left位置的值 + 后手的值(left + 1 ~ right后手);若拿right,值就是right位置的值 + 后手的值(left ~ right - 1后手),返回二者值中较大的
(3)后手分为对手拿左还是右,若对手拿left,值就是先手的值(left + 1 ~ right先手);若拿right,值就是先手的值(left ~ right - 1先手),因为对手一定拿最优解,那么后手一定是二者值中较小的情况
(4)若 left == right 先手就是 left位置的值,后手就是0
public static int win(int[] arr){
if (arr == null || arr.length == 0){
return 0;
}
int valueA = first(arr, 0, arr.length - 1); //A的先手情况
int valueB = second(arr, 0, arr.length - 1); //B的后手情况
return Math.max(valueA, valueB);
}
public static int first(int[] arr, int left, int right){
//先手获得的最好分数
if (left == right){
//递归结束条件
return arr[left];
}
int l = arr[left] + second(arr, left + 1, right); //拿左边
int r = arr[right] + second(arr, left, right - 1); //拿右边
return Math.max(l, r);
}
public static int second(int[] arr, int left, int right){
//先手获得的最好分数
if (left == right){
//递归结束条件
return 0;
}
int l = first(arr, left + 1, right); //对手拿走了左边
int r = first(arr, left, right - 1); //对手拿走了右边
//因为对手一定会挑选最优情况,所以留给后手一定是俩者中较差的选择
return Math.min(l, r);
}
- 结果演示:
arr = [1, 100, 2]
arr = [1, 2, 100, 4]
题目五:递归逆序栈
- 思维:
(1)利用系统栈保存数据的性能解决问题
(2)首先要实现得到栈底元素的函数,使用递归一直调用该函数是元素出栈,若栈为空返回栈底元素, 否则返回递归的临时变量
(3)逆序函数的实现,一次逆序就是得到栈底元素,递归调用,直至栈为空,重新压入系统栈的保存数据
public static void reverse(Stack<Integer> stack){
//逆序栈
if (stack.isEmpty()){
//栈空返回
return;
}
//得到栈底元素
int bottom = getBottom(stack);
//递归调用
reverse(stack);
//栈空后重新压入,利用系统栈的保存数据
stack.push(bottom);
}
public static int getBottom(Stack<Integer> stack){
//得到栈底元素
int result = stack.pop();
if (stack.isEmpty()){
//到达栈底返回
return result;
}
//递归调用
int temp = getBottom(stack);
//将非栈底元素重新压入栈
stack.push(result);
//temp在系统栈中保存递归数据
return temp;
}
- 结果演示:
栈中元素为:1,2,3
题目六:数字与字符串的转化问题
- 思路:
从左往右的尝试
在数组中index为当前遍历到的位置,尝试index位置的所有可能出现的情况后,index继续移动,直至数组结束即可 - 情况分析:
(1)若index位置出现0,说明前面转化出错,返回0
(2)若index位置为 1 或 2,可以考虑与 index + 1位置结合转化,也可以单独转化,产生两种递归的方式:直接到下一个位置,或者跳过下一个位置
(3)若index位置为3 ~ 9,就直接单独转化,没有产生多余的情况,直接递归到下一个位置
(4)若index等于数组长度,说明此次转化结束返回 1
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 index){
//遍历数组
if (index == chs.length){
//index到最后一个说明此次递归得到了一种情况
return 1;
}
if (chs[index] == '0'){
//index位置出现0,说明此次递归出错
return 0;
}
if (chs[index] == '1'){
//index位置出现1
//单独转化index位置
int result = process(chs, index + 1);
if (index + 1 < chs.length){
//结合index + 1位置转化
result += process(chs, index + 2);
}
return result;
}
if (chs[index] == '2') {
//index位置出现2
//单独转化index位置
int result = process(chs, index + 1);
if (index + 1 < chs.length && (chs[index + 1] <= '6')){
//结合index位置转化
//该转化时,数字小于等于26
result += process(chs, index + 2);
}
return result;
}
//index位置为 3 ~ 9
return process(chs, index + 1);
}
- 结果演示:
str = “121451202”
题目七:重量和价值问题
- 思路:
尝试位置0 ~ i,分为要和不要
(1)记录当前重量,若超过bag,则无法获得
(2)若要,当前重量增加,价值增加,遍历下一个;若不要直接遍历到下一个
(3)若已经将全部都遍历完则返回0
public static int maxValue(int[] weights, int[] values, int bag){
if (weights.length == 0){
return 0;
}
return process(weights, values, bag, 0, 0);
}
public static int process(int[] weights, int[] values, int bag, int index, int alreadyWeight){
//index:目前遍历到的位置
//alreadyWeight:目前的重量
if (index == values.length){
//已经全部遍历完
return 0;
}
int getValue;
if (alreadyWeight + weights[index] <= bag){
//当前仍然可以增加index的重量
//要index位置的物品,alreadyWeight增加,返回value的增加
getValue = values[index] + process(weights, values, bag, index + 1, alreadyWeight + weights[index]);
}else {
//无法增加,与noValue相同
getValue = process(weights, values, bag, index + 1, alreadyWeight);
}
//不要index位置的物品,value, alreadyWeight均不变
int noValue = process(weights, values, bag, index + 1, alreadyWeight);
//决策出两者的较大值,返回
return Math.max(getValue, noValue);
}
- 结果演示:
weights = [10, 3, 7, 8, 9]
values = [4, 2, 3, 9, 5]
bag = 20
题目八:N皇后问题
- 思路:
(1)尝试在每一行的不同列位置放一个皇后
(2)要求:以后放的都不能与前面的同列或者同斜线
public static int num(int n){
if (n < 0){
return 0;
}
//record[i]表示第 i - 1行的皇后放在第几列
int[] record = new int[n];
return process(0, record, n);
}
public static int process(int index, int[] record, int n){
//index目前遍历到的行
//n为棋盘大小
if (index == n){
//遍历正常完成,之前的摆放正确
return 1;
}
int result = 0;
for (int row = 0; row < n; row++){
//当前index行,皇后放到row列
if (isValid(record, index, row)){
//若位置有效
record[index] = row;
result += process(index + 1, record, n);
}
}
return result;
}
public static boolean isValid(int[] record, int index, int row){
//判断加入的皇后是否合法
for (int i = 0; i < index; i++){
//遍历之前行加入的皇后
if (record[i] == row || Math.abs(record[i] - row) == Math.abs(i - index)){
//判断是否同列,或者同斜线
//同列的判断:两点的斜率是否为1(45度),或者-1(135度)
//就是行差的绝对值与列差的绝对值是否相同
return false;
}
}
return true;
}
- 结果演示:
n = 8