1.汉诺塔问题
相传在古印度圣庙中,有一种被称为汉诺塔(Hanoi)的游戏。该游戏是在一块铜板装置上,有三根杆(编号A、B、C),在A杆自下而上、由大到小按顺序放置64个金盘。游戏的目标:把A杆上的金盘全部移到C杆上,并仍保持原有顺序叠好。操作规则:每次只能移动一个盘子,并且在移动过程中三根杆上都始终保持大盘在下,小盘在上,操作过程中盘子可以置于A、B、C任一杆上,
我们先看一个比较好理解的版本
我们假如说有三个金盘自上而下分别为 1 2 3,第一大步我们需要把 1 2 移动到B杆,然后把3移动到C杆,第二大步,我们需要把 3 移动到 C 杆 第三大步,我们再把 1 2移动到C杆这样我们就能把所有的金盘移动到C杆了。下面是最简单的写法
public static void hanoi1(int n){
leftToRight(n);
}
// 1 - n 从左到右
private static void leftToRight(int n) {
// base case 如果只有一个,就直接移动
if (n == 1){
System.out.println("Move 1 from left to right");
return;
}else{
//不为一个的时候,先把 n - 1 个从左移到中
leftToMid(n - 1);
//把剩下那一个直接移动
System.out.println("move "+n+" from left to right ");
//把刚刚的n-1从中移动到右
midToRight(n - 1);
}
}
private static void midToRight(int n) {
if (n == 1){
System.out.println("Move 1 from mid to right");
return;
}else {
midToLeft(n - 1);
System.out.println("Move"+ n +"from mid to right");
leftToRight(n - 1);
}
}
private static void midToLeft(int n) {
if (n == 1){
System.out.println("mid 1 from mid to left");
return;
}else {
midToRight( n - 1);
System.out.println("move"+n+"from mid to left");
leftToRight(n-1);
}
}
// 1 - n 从左到中
private static void leftToMid(int n) {
if (n == 1){ //如果是一个直接挪
System.out.println("Move 1 from left To Mid");
return;
}else {
//如果不是一个,先把n-1从左到右
leftToRight( n - 1);
//然后把那一个从左移到中
System.out.println("Move "+ n + "from left to mid");
//然后把n-1从右移动到中
rightToMid(n - 1);
}
}
private static void rightToMid(int n) {
if (n == 1){
System.out.println("Move 1 from right to mid");
}else {
rightToLeft(n - 1);
System.out.println("move"+n+"from right to mid");
leftToRight( n - 1);
}
}
private static void rightToLeft(int n) {
if (n == 1){
System.out.println("Move 1 from right to left");
}else {
rightToMid(n - 1);
System.out.println("Move"+n+"from right to left");
midToLeft( n - 1);
}
}
看起来很简单,很好理解对吧,但是我们看这六个过程是有些麻烦是吧,我们仔细看,只有一个参数却要六个过程,我们来进行一个抽象,我们假设没有左中右,1-N都在from上,我们的目标是to,1-N,我们要从from 挪到to中去,我们引进一个中间的other,第一大步就是 1 到 n - 1 从 from 到 other 中,第二步 将 n 从 from 移动到 to 第三步在将 1 到 n-1 从other移动到to中去,这样是不是就全都到to中了。
public static void hanoi2(int n,String from,String to,String other){
if (n == 1){
System.out.println("Move 1 form"+from+"to"+to);
}else {
hanoi2(n - 1,from,other,to);
System.out.println("Move" + n +"from"+"to"+to);
hanoi2(n-1,other,to,from);
}
}
经过抽象之后,发现代码是不是少了很多,我们可以通过增加参数来增加递归更多的可能性。
2.打印一个字符串全部的子序列
什么叫子序列?可以不是连续的,例如 1 2 3 4 5 6 , 1 2 , 1 3 ,2 5 ,等等,只要能找得到,都算子序列
public static List<String> subs(String s){
char[] chars = s.toCharArray();
String path = "";
List<String> ans = new ArrayList<>();
process(chars,0,ans,path);
return ans;
}
// chars 固定参数
//来到str[index]字符 index 是位置
//str[0...index-1]已经走过了,之前的决定,都在path上
//str[index...] 还能决定,之前已经确定了,而后面还能自由选择的话
//把所有生成的子序列,放入到ans中
private static void process(char[] chars, int index, List<String> ans, String path) {
if (index == chars.length){
ans.add(path);
return;
}else {
//没要index 位置的字符
process(chars,index+1,ans,path);
//要了index 位置字符
process(chars,index+1,ans,path+chars[index]);
}
}
3.打印一个字符串全部的子序列,要求不要出现重复字面值的子序列
有了上面的方法,这题就很好做了,我们把添加的结果放到set中,这样就没有重复值,最后再转成list返回即可。
public static List<String> subs1(String s){
char[] chars = s.toCharArray();
String path = "";
Set<String> ans = new HashSet<>();
process1(chars,0,ans,path);
List<String> list = new ArrayList<>();
for (String set : ans) {
list.add(set);
}
return list;
}
// chars 固定参数
//来到str[index]字符 index 是位置
//str[0...index-1]已经走过了,之前的决定,都在path上
//str[index...] 还能决定,之前已经确定了,而后面还能自由选择的话
//把所有生成的子序列,放入到ans中
private static void process1(char[] chars, int index, Set<String> ans, String path) {
if (index == chars.length){
ans.add(path);
return;
}else {
//没要index 位置的字符
process1(chars,index+1,ans,path);
//要了index 位置字符
process1(chars,index+1,ans,path+chars[index]);
}
}
4.打印一个字符串的全部排列
假如说传入一个字符串 abc ,我们就应该返回[abc, acb, bac, bca, cab, cba]
这里面用到递归中非常常用的一个技巧,叫做恢复现场。
public static List<String> permutation1(String str){
List<String> result = new ArrayList<>();
if (str == null || str.length() == 0){
return result;
}
char[] chars = str.toCharArray();
ArrayList<Character> characters = new ArrayList<>();
for (int i = 0; i < chars.length; i++) {
characters.add(chars[i]);
}
String path = "";
fun(characters,path,result);
return result;
}
public static void fun(ArrayList<Character> rest,String path,List<String> ans){
if (rest.isEmpty()){
ans.add(path);
}else {
int N = rest.size();
for (int i = 0; i < N; i++) {
char cur = rest.get(i);
rest.remove(i);
fun(rest,path+cur,ans);
rest.add(i,cur);
}
}
}
这个递归看起来不是很好,为什么呢,因为递归最讲究的是可变参数的设计,只有可变参数怎么评价优劣,我们再以后再讲,下面介绍一个好一点的递归。
public static List<String> permutation2(String str){
List<String> result = new ArrayList<>();
if (str == null || str.length() == 0){
return result;
}
char[] chars = str.toCharArray();
func(chars,0,result);
return result;
}
public static void func(char[] str,int index,List<String> ans){
if (index == str.length){
ans.add(String.valueOf(str));
}else {
for (int i = index; i < str.length; i++) {
swap(str,i,index);
func(str,index+1,ans);
swap(str,i,index);
}
}
}
public static void swap(char[] strs,int i,int j){
char temp = strs[i];
strs[i] = strs[j];
strs[j] = temp;
}
5.打印一个字符串的全部排列,要求不出现重复的排列
public static List<String> permutation3(String str){
List<String> result = new ArrayList<>();
if (str == null || str.length() == 0){
return result;
}
char[] chars = str.toCharArray();
func3(chars,0,result);
return result;
}
public static void func3(char[] str,int index,List<String> ans){
if (index == str.length){
ans.add(String.valueOf(str));
}else {
boolean[] visited = new boolean[256];
for (int i = index; i < str.length; i++) {
if (!visited[str[i]]){
visited[str[i]] = true;
swap(str,i,index);
func(str,index+1,ans);
swap(str,i,index);
}
}
}
}
public static void swap(char[] strs,int i,int j){
char temp = strs[i];
strs[i] = strs[j];
strs[j] = temp;
}
6.给你一个栈,请你逆序这个栈,不能申请额外数据结构,只能使用递归函数,如何实现?
public static void reverse(Stack<Integer> stack){
if (stack.isEmpty()){
return;
}
int i = f(stack);
reverse(stack);
stack.push(i);
}
//栈底元素移除
//上面元素盖下来
//返回移除掉的栈底元素
public static int f(Stack<Integer> stack){
int result = stack.pop();
if (stack.isEmpty()){
return result;
}else {
int last = f(stack);
stack.push(result);
return last;
}
}