数据结构基础之《(23)—暴力递归》

一、什么是暴力递归

1、暴力递归
(1)把问题转化为规模缩小了的同类问题的子问题
    把一个大问题拆成小问题
(2)有明确的不需要继续进行递归的条件(base case)
    这个问题小的什么样的规模就不要继续了
(3)有当得到了子问题的结果之后的决策过程
    得到了每一个子问题的结果回溯完毕,怎么做决策
(4)不记录每一个子问题的解
    不记录子问题的解就是暴力,记录子问题的解就是动态规划

二、熟悉什么叫尝试

1、打印n层汉诺塔从最左边移动到最右边的全部过程

汉诺塔问题:
要么杆上没有圆盘,可以放
要么只能小压大
圆盘在左中右三个杆可以互相移动

1到N层的圆盘怎么移动到右边:
(1)第一步,1到N-1个圆盘移动到中间
(2)第二步,N层圆盘自己从左边移动到右边
(3)第三步,1到N-1层圆盘,从中间移动到右边

代码:

package class12;

/**
 * 汉诺塔问题
 */
public class Code01_Hanoi {

	/**
	 * 方法一
	 * @param n
	 */
	public static void hanoi1(int n) {
		leftToRight(n);
	}
	
	/**
	 * 主函数:把1~N层圆盘,从左->右
	 * @param n
	 */
	public static void leftToRight(int n) {
		if (n == 1) {
			System.out.println("Move 1 from left to right");
			return;
		}
		leftToMid(n - 1); //1到n-1的圆盘从最左移到中间
		System.out.println("Move " + n + " from left to right"); //第n层圆盘从最左移到右边
		midToRight(n - 1); //把n-1层圆盘从中间移到右边来
	}
	
	/**
	 * 把N层圆盘,从左->中
	 * @param n
	 */
	public static void leftToMid(int n) {
		if (n == 1) {
			System.out.println("Move 1 from left to mid");
			return;
		}
		leftToRight(n - 1); //把n-1层圆盘,从最左移到最右
		System.out.println("Move " + n + " from left to mid"); //第n层圆盘从最左移到中间
		rightToMid(n - 1); //把n-1层圆盘,从最右移到中间
	}
	
	public static void rightToMid(int n) {
		if (n == 1) {
			System.out.println("Move 1 from right to mid");
			return;
		}
		rightToLeft(n - 1);
		System.out.println("Move " + n + " from right to mid");
		leftToMid(n - 1);
	}
	
	public static void midToRight(int n) {
		if (n == 1) {
			System.out.println("Move 1 from mid to right");
			return;
		}
		midToLeft(n - 1);
		System.out.println("Move " + n + " from mid to right");
		leftToRight(n - 1);
	}
	
	public static void midToLeft(int n) {
		if (n == 1) {
			System.out.println("Move 1 from mid to left");
			return;
		}
		midToRight(n - 1);
		System.out.println("Move " + n + " from mid to left");
		rightToLeft(n - 1);
	}
	
	public static void rightToLeft(int n) {
		if (n == 1) {
			System.out.println("Move 1 from right to left");
			return;
		}
		rightToMid(n - 1);
		System.out.println("Move " + n + " from right to left");
		midToLeft(n - 1);
	}
	
	/**
	 * 方法二
	 * @param n
	 */
	public static void hanoi2(int n) {
		if (n > 0) {
			func(n, "left", "right", "mid");
		}
	}
	
	/**
	 * 1~i 圆盘
	 * 目标是from -> to
	 * other是另外一个
	 * @param N
	 * @param from
	 * @param to
	 * @param other
	 */
	public static void func(int N, String from, String to, String other) {
		if (N == 1) {
			System.out.println("Move 1 from " + from + " to " + to);
		} else {
			func(N - 1, from, other, to);
			System.out.println("Move " + N + " from " + from + " to " + to);
			func(N - 1, other, to, from);
		}
	}
	
	public static void main(String[] args) {
		int n = 3;
		hanoi1(n);
		System.out.println("====================");
		hanoi1(n);
		System.out.println("====================");
	}
}

说明:
方法一,hanoi1函数,6个递归相互嵌套,主函数是leftToRight
方法二,hanoi2函数,忘掉左中右,只有from、to、other,考虑1到N层圆盘怎么从from到to
(1)第一步,1到N-1层圆盘从from移动到other
(2)第二步,N层圆盘从from移动到to
(3)第三步,1到N-1层圆盘从other移动到to

2、打印一个字符串的全部子序列

PS:子串和子序列的区别
子串是连续的字符串,子序列是在原始序列中依次拿字符,可以不连续,要求相对顺序不能乱

把一个位置的字符,要和不要的情况彻底的展开,就是子序列

package class12;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;

/**
 * 打印子序列
 */
public class Code02_PrintAllSubsquences {

	/**
	 * 打印一个字符串的全部子序列
	 * @param s
	 * @return
	 */
	public static List<String> subs(String s) {
		char[] str = s.toCharArray();
		String path = "";
		List<String> ans = new ArrayList<>();
		process1(str, 0, ans, path);
		return ans;
	}
	
	/**
	 * 
	 * @param str 固定参数
	 * @param index 现在来到的位置,当前字符 要or不要 做决策
	 * @param ans 如果index来到了str中的终止位置,要把沿途路径形成的答案扔到answer里去
	 * @param path 之前作出的选择,就是path,沿途路径
	 */
	public static void process1(char[] str, int index, List<String> ans, String path) {
		if (index == str.length) { //当index到终止位置
			ans.add(path);
			return;
		}
		String no = path;
		//所有路径都是存到同一个ans变量里
		process1(str, index + 1, ans, no); //不把index的字符往下传
		String yes = path + String.valueOf(str[index]);
		process1(str, index + 1, ans, yes); //把index的路径往下传
	}
	
	/**
	 * 打印一个字符串的全部子序列,要求不要出现重复字面值的子序列
	 * @param s
	 * @return
	 */
	public static List<String> subsNoRepeat(String s) {
		char[] str = s.toCharArray();
		String path = "";
		HashSet<String> set = new HashSet<>();
		process2(str, 0, set, path);
		List<String> ans = new ArrayList<>();
		for (String cur : set) {
			ans.add(cur);
		}
		return ans;
	}
	
	public static void process2(char[] str, int index, HashSet<String> set, String path) {
		if (index == str.length) {
			set.add(path);
			return;
		}
		String no = path;
		process2(str, index + 1, set, no);
		String yes = path + String.valueOf(str[index]);
		process2(str, index + 1, set, yes);
	}
	
	public static void main(String[] args) {
		String str1 = "abc";
		List<String> list1 = subs(str1);
		for(String s : list1) {
			System.out.print(" " + s);
		}
		System.out.println();
		String str2 = "aaa";
		List<String> list2 = subsNoRepeat(str2);
		for (String s : list2) {
			System.out.print(" " + s);
		}
	}
	
}

3、打印一个字符串的全部子序列,要求不要出现重复字面值的子序列
见上面代码

4、打印一个字符串的全部排列

package class12;

import java.util.ArrayList;
import java.util.List;

/**
 * 打印全排列
 */
public class Code03_PrintAllPermutations {

	/**
	 * 打印一个字符串的全部排列
	 * @param str
	 * @return
	 */
	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;
	}
	
	/**
	 * str[i]及其往后的所有字符,都有机会来到i位置
	 * str[0]到[i]位置已经做好决定的
	 * i终止位置,str当前的样子,就是一种结果,放入ans
	 * @param str
	 * @param i
	 * @param ans
	 */
	public static void process(char[] str, int i, ArrayList<String> ans) {
		if (i == str.length) {
			ans.add(String.valueOf(str));
		}
		//如果i没有到终止位置,从i往后所有的位置都可以来到i位置
		for (int j = i; j < str.length; j++) {
			swap(str, i, j);
			process(str, i + 1, ans);
			swap(str, i, j); //恢复现场
		}
	}
	
	/**
	 * 打印一个字符串的全部排列,要求不要出现重复的排列
	 * @param str
	 * @return
	 */
	public static ArrayList<String> permutationNoRepeat(String str) {
		ArrayList<String> res = new ArrayList<>();
		if (str == null || str.length() == 0) {
			return res;
		}
		char[] chs = str.toCharArray();
		process2(chs, 0, res);
		return res;
	}
	
	public static void process2(char[] str, int i, ArrayList<String> res) {
		if (i == str.length) {
			res.add(String.valueOf(str));
		}
		boolean[] visit = new boolean[26]; //分支限界,这个字符是否已经出现过
		for (int j = i; j < str.length; j++) {
			if (!visit[str[j] - 'a']) {
				visit[str[j] - 'a'] = true; //这种字符没出现过,登记它
				swap(str, i, j);
				process2(str, i + 1, res);
				swap(str, i, j);
			}
		}
	}
	
	public static void swap(char[] chs, int i, int j) {
		char tmp = chs[i];
		chs[i] = chs[j];
		chs[j] = tmp;
	}
	
	public static void main(String[] args) {
		String s = "aac";
		List<String> ans1 = permutation(s);
		for (String str : ans1) {
			System.out.print(" " + str);
		}
		System.out.println();
		List<String> ans2 = permutationNoRepeat(s);
		for (String str : ans2) {
			System.out.print(" " + str);
		}
	}
}

5、打印一个字符串的全部排列,要求不要出现重复的排列
见上面代码

三、仰望好的尝试

1、给你一个栈,请你逆序这个栈,不能申请额外的数据结构,只能使用递归函数。如何实现?

package class12;

import java.util.Stack;

/**
 * 用递归,栈逆序
 */
public class Code04_ReverseStackUsingRecursive {

	public static void reverse(Stack<Integer> stack) {
		if (stack.isEmpty()) {
			return;
		}
		int i = func(stack);
		reverse(stack);
		stack.push(i);
	}
	
	public static int func(Stack<Integer> stack) {
		int result = stack.pop(); //从栈中拿出一个
		if (stack.isEmpty()) {
			return result;
		} else {
			int last = func(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());
		}
	}
	
}

说明:
揭示递归栈是可以帮你保存一些信息的

四、从左往右的尝试模型

1、背包问题
给定两个长度都为N的数组weights和values,weights[i]和values[i]分别代表i号物品的重量和价值。给定一个整数bag,表示一个载重bag的袋子,你装的物品不能超过这个重量。返回你能装下最多的价值是多少?

package class12;

/**
 * 背包问题
 */
public class Code07_Knapsack {

	/**
	 * 方式一
	 * @param w
	 * @param v
	 * @param bag
	 * @return
	 */
	public static int getMaxValue(int[] w, int[] v, int bag) {
		return process(w, v, 0, 0, bag);
	}
	
	/**
	 * 
	 * @param w 物品的重量
	 * @param v 物品的价值
	 * @param index 当前物品
	 * @param alreadyW 之前做的选择,已经达到的重量是多少
	 * @param bag 背包的总载重
	 * @return
	 */
	public static int process(int[] w, int[] v, int index, int alreadyW, int bag) {
		if (alreadyW > bag) {
			return -1; //返回-1表示没有这种方案
		}
		//到了终止位置,重量没超
		if (index == w.length) {
			return 0;
		}
		//两种选择
		//第一种选择,没有要当前物品的情况下,后续的最大价值
		int p1 = process(w, v, index + 1, alreadyW, bag);
		//第二种选择,要了当前的物品,后续的最大价值
		int p2next = process(w, v, index + 1, alreadyW + w[index], bag);
		int p2 = -1;
		if (p2next != -1) { //如果后面的过程不是无效的
			p2 = v[index] + p2next; //可能性二的价值
		}
		return Math.max(p1, p2);
	}
	
	/**
	 * 方式二
	 * @param w
	 * @param v
	 * @param bag
	 * @return
	 */
	public static int maxValue(int[] w, int[] v, int bag) {
		return process(w, v, 0, bag);
	}
	
	//只剩下rest的空间了
	//index及其往后的货物自由选择,但是剩余空间不要小于0
	//返回能够获得的最大价值
	public static int process(int[] w, int[] v, int index, int rest) {
		if (rest <= 0) { //base case 1
			return -1;
		}
		if (index == w.length) {//rest>=0,base case 2
			return 0;
		}
		//有货也有空间,base case 3
		int p1 = process(w, v, index + 1, rest);
		int p2 = Integer.MIN_VALUE; //p2先认为是一个无效方案
		if (rest >= w[index]) {
			p2 = v[index] + process(w, v, index + 1, rest - w[index]);
		}
		return Math.max(p1, p2);
	}
}

五、范围上的尝试模型

1、给定一个整型数组arr,代表数值不同的纸牌排成一条线,玩家A和玩家B依次拿走每张纸牌,规定玩家A先拿,玩家B后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家A和玩家B都绝顶聪明。请返回最后获胜者的分数。
例如:70,100,1,4

先手:
定义S函数S(arr,L,R)
(1)如果只剩一张牌,直接拿走
(2)如果剩下不止一张牌
max(arr[L]+S(arr,L+1,R), arr[R]+S(arr,L,R-1))

后手:
定义f函数f(arr,L,R)
(1)如果只剩一张牌,返回0
(2)如果剩下不止一张牌
min(f(arr,L+1,R), f(arr,L,R-1))

package class12;

/**
 * 拿纸牌问题
 */
public class Code08_CardsInLine {

	/**
	 * 方法一(递归)
	 * @param arr
	 * @return
	 */
	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));
	}
	
	/**
	 * 先手的函数
	 * @param arr
	 * @param L
	 * @param R
	 * @return
	 */
	public static int f(int[] arr, int L, int R) {
		if (L == R) { //只剩一张牌
			return arr[L];
		}
		//有牌能选
		return Math.max(
				//当我拿走左边的牌,轮到后手拿L+1到R的牌
				arr[L] + s(arr, L + 1, R), 
				//当我拿走右边的牌,轮到后手拿L到R-1的牌
				arr[R] + s(arr, L, R - 1));
	}
	
	/**
	 * 后手的函数
	 * @param arr
	 * @param L
	 * @param R
	 * @return
	 */
	public static int s(int[] arr, int L, int R) {
		if (L == R) { //只剩一张牌
			return 0;
		}
		//对手肯定会扔给你最差的
		return Math.min(
				f(arr, L + 1, R), //对手拿了arr[L],扔给你的在L+1到R范围拿的牌,相当于你先手
				f(arr, L, R - 1)); //对手拿了arr[R],扔给你的在L到R-1范围拿的牌,相当于你先手
	}
	
	/**
	 * 方法二(非递归)
	 * @param arr
	 * @return
	 */
	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[0][arr.length - 1], s[0][arr.length - 1]);
	}
	
	public static void main(String[] args) {
		int[] arr = {70, 100, 1, 4};
		System.out.println(win1(arr));
		System.out.println(win2(arr));
	}
	
}

六、N皇后问题

1、N皇后问题是指在N*N的棋盘上要摆N个皇后,要求任何两个皇后不同行、不同列,也不在同一条斜线上
给定一个整数n,返回n皇后的摆法有多少种。
例如:
n=1,返回1
n=2或3,2皇后和3皇后问题无论怎么摆都不行,返回0
n=8,返回92

package class12;

/**
 * N皇后问题
 */
public class Code09_NQueens {

	public static int num1(int n) {
		if (n < 1) {
			return 0;
		}
		int[] record = new int[n]; //record[i] -> i行的皇后,放在了第几行
		return process1(0, record, n);
	}
	
	// 潜台词:record[0...i-1]的皇后,任何两个皇后一定都不共行、不共列、不共斜线
	// 目前来到了第i行
	// record[0...i-1]表示之前的行,放了皇后的位置
	// n代表整体一共有多少行
	// 返回值是,摆完所有的皇后,合理的摆法有多少种
	public static int process1(int i, int[] record, int n) {
		if (i == n) { //终止行
			return 1;
		}
		//没有到终止位置,还有皇后要摆
		int res = 0;
		for (int j = 0; j < n; j++) {
			// 当前行在i行,尝试i行所有的列j
			// 当i行的皇后放在j列,会不会和之前[0...i-1]的皇后,不共行共列或共斜线
			// 如果是,认为有效
			// 如果不是,认为无效
			if (isValid(record, i, j)) {
				record[i] = j;
				res += process1(i + 1, record, n);
			}
		}
		return res;
	}
	
	/**
	 * 判断当前摆的皇后,是否共行共列或共斜线
	 * @param record [0...i-1]的皇后
	 * @param i 要摆的皇后在i行
	 * @param j 要摆的皇后在j列
	 * @return 返回i行皇后,放在了j列,是否有效
	 */
	public static boolean isValid(int[] record, int i, int j) {
		for (int k = 0; k < i; k++) {
			// 肯定不共行
			// j == record[k]:判断是否共列
			// |行-行|=|列-列|:判断是否共斜线
			if (j == record[k] || Math.abs(record[k] - j) == Math.abs(i - k)) {
				return false;
			}
		}
		return true;
	}
	
	public static void main(String[] args) {
		int n = 13;
		long start = System.currentTimeMillis();
		System.out.println(num1(n));
		long end = System.currentTimeMillis();
		System.out.println("cost time: " + (end - start) + "ms");
	}
}

七、递归小结
不需要打断点分析明白,那么多栈帧,把递归看成黑盒,注意下return的条件就行了
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值