递归思想非常重要,计算机编程中很多算法都是要用到递归完成,同时递归也是解决很多问题的思路,比如动态规划,就是在递归的基础上,缓存结果,达到阶梯的目的。本文总结了一下递归尝试的常用题目和思路,方便深入的理解递归。
1、暴力递归尝试
- 1,把问题转化为规模缩小了的同类问题的子问题
- 2,有明确的不需要继续进行递归的条件(base case),否则就会无限递归
- 3,有当得到了子问题的结果之后的决策过程
- 4,不记录每一个子问题的解,记录并应用,就是动态规划
2、暴力递归尝试例题
2.1、打印n层汉诺塔从最左边移动到最右边的全部过程
2.1.1、方法一
- 方法一:从左到右的暴力递归方法
- 思路:
- 汉诺塔从上往下依次是1到n层,按照规则,大的只能在下面,如果要将整体从左侧移到右侧,就需要先将n-1层以上的从左侧移动到中间,
- 然后将n层移动到右侧,再将n-1层以上的从中间移动到右侧,如此递归下去,就能得到结果。
- 过程:
- 1,把n-1层汉诺塔从最左边移动到中间
- 2,把第n层汉诺塔从最左边移动到最右边
- 3,把n-1层汉诺塔从中间移动到最右边
- 时间复杂度:O(2^n - 1)
java
/**
* 方法一:从左到右的暴力递归方法
* 思路:
* 汉诺塔从上往下依次是1到n层,按照规则,大的只能在下面,如果要将整体从左侧移到右侧,就需要先将n-1层以上的从左侧移动到中间,
* 然后将n层移动到右侧,再将n-1层以上的从中间移动到右侧,如此递归下去,就能得到结果。
* 过程:
* 1,把n-1层汉诺塔从最左边移动到中间
* 2,把第n层汉诺塔从最左边移动到最右边
* 3,把n-1层汉诺塔从中间移动到最右边
* 时间复杂度:O(2^n - 1)
*/
public static void hanoi1(int n) {
if (n > 0) {
leftToRight(n);
}
}
/**
* 将n层从左移动到右侧
*/
private static void leftToRight(int n) {
if (n == 1) {
// base case,只有一层,直接移动过去
System.out.println("Move 1 from left to right");
return;
}
// 先将n-1从左侧移动到中间
leftToMid(n - 1);
// 将n从左侧移动到右侧
System.out.println("Move " + n + " from left to right");
// 将n-1从中间移动到右侧
midToRight(n - 1);
}
/**
* 将n层从中间移动到右侧
*/
private static void midToRight(int n) {
if (n == 1) {
System.out.println("Move 1 from mid to right");
return;
}
// 将n-1层从中间移动到左侧
midToLeft(n - 1);
// 将n层从中间移动到右侧
System.out.println("Move " + n + " from mid to right");
// 将n-1层从左侧移动到右侧
leftToRight(n - 1);
}
/**
* 将n层从左侧移动到中间
*/
private static void leftToMid(int n) {
if (n == 1) {
System.out.println("Move 1 from left to mid");
return;
}
// 将n-1层从左侧移动到右侧
leftToRight(n - 1);
// 将n层从左侧移动到中间
System.out.println("Move " + n + " from left to mid");
// 将n-1层从右侧移动到中间
rightToMid(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 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 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);
}
2.1.2、方法二
- 方法二:在方法一的基础上简化递归函数
- 思路:
- 汉诺塔问题中,一共有三个位置,分别是左侧、中间、右侧,在每一次移动的过程中,一个是源位置,一个是目标位置,剩下的一个就是中间的临时过度的位置。
- 在移动的过程中,如果想把n层从源位置移动到目标位置,就要先将n-1层从源移动到临时过渡位置,然后将n层从原位置移到到目标位置,最后将原来移动到临时位置的从临时位置移动到目标位置。
- 在这个递归过程中,三个位置的角色是相互变化的,可以写成一个函数。
- 在方法一中,将移动的方向写成了函数,导致函数特别多,其实我们可以将方向用参数传递,
- 用from表示n层的起始位置,用to表示要到达的位置,temp表示临时可用的位置,这样在递归函数中交换不同的位置,就可以将函数抽象成一个。
- 方法一和方法二的复杂度是一样的,只是code的代码量不同。
java
/**
* 方法二:在方法一的基础上简化递归函数
* 思路:
* 汉诺塔问题中,一共有三个位置,分别是左侧、中间、右侧,在每一次移动的过程中,一个是源位置,一个是目标位置,剩下的一个就是中间的临时过度的位置。
* 在移动的过程中,如果想把n层从源位置移动到目标位置,就要先将n-1层从源移动到临时过渡位置,然后将n层从原位置移到到目标位置,最后将原来移动到临时位置的从临时位置移动到目标位置。
* 在这个递归过程中,三个位置的角色是相互变化的,可以写成一个函数。
* 在方法一中,将移动的方向写成了函数,导致函数特别多,其实我们可以将方向用参数传递,
* 用from表示n层的起始位置,用to表示要到达的位置,temp表示临时可用的位置,这样在递归函数中交换不同的位置,就可以将函数抽象成一个。
* 方法一和方法二的复杂度是一样的,只是code的代码量不同。
*/
public static void hanoi2(int n) {
if (n > 0) {
hanoiProcess(n, "left", "right", "mid");
}
}
/**
* 汉诺塔递归函数
*
* @param n :层数编号
* @param from :源位置
* @param to :目标位置
* @param temp :临时过渡位置
*/
private static void hanoiProcess(int n, String from, String to, String temp) {
if (n == 1) {
System.out.println("Move 1 from " + from + " to " + to);
return;
}
// 先将n-1从from移动到temp,中间位置就是to
hanoiProcess(n - 1, from, temp, to);
// 再将n从from移动到to
System.out.println("Move " + n + " from " + from + " to " + to);
// 最后将n-1从temp移动到to,中间位置就是from
hanoiProcess(n - 1, temp, to, from);
}
2.1.4、方法三
- 方法三:汉诺塔的非递归实现
- 思路:
- 要将递归函数改成非递归,就是要将打印的过程记录下来,然后用栈模拟递归的过程。
- 我们可以将打印的过程封装成一个Record对象,然后依次将这些对象压入栈中,模拟递归的过程。
- 在这个过程中,还要判断是不是最后一层,就是base case的情况,这个过程比较复杂,具体思路如下:
- 把汉诺塔问题想象成二叉树
- 比如当前还剩i层,其实打印这个过程就是:
-
- 去打印第一部分 -> 左子树
-
- 打印当前的动作 -> 当前节点
-
- 去打印第二部分 -> 右子树
- 那么你只需要记录每一个任务 : 有没有加入过左子树的任务
- 就可以完成迭代对递归的替代了
java
/**
* 方法三:汉诺塔的非递归实现
* 思路:
* 要将递归函数改成非递归,就是要将打印的过程记录下来,然后用栈模拟递归的过程。
* 我们可以将打印的过程封装成一个Record对象,然后依次将这些对象压入栈中,模拟递归的过程。
* 在这个过程中,还要判断是不是最后一层,就是base case的情况,这个过程比较复杂,具体思路如下:
* 把汉诺塔问题想象成二叉树
* 比如当前还剩i层,其实打印这个过程就是:
* 1) 去打印第一部分 -> 左子树
* 2) 打印当前的动作 -> 当前节点
* 3) 去打印第二部分 -> 右子树
* 那么你只需要记录每一个任务 : 有没有加入过左子树的任务
* 就可以完成迭代对递归的替代了
*/
public static void hanoi3(int N) {
if (N < 1) {
return;
}
// 定义一个记录的栈
Stack<Record> stack = new Stack<>();
// 记录每一个记录有没有加入过左子树的任务
Set<Record> finishLeft = new HashSet<>();
// 初始的任务,认为是种子
stack.add(new Record(N, "left", "right", "mid"));
while (!stack.isEmpty()) {
// 弹出当前任务
Record cur = stack.pop();
if (cur.level == 1) {
// 如果层数只剩1了
// 直接打印
System.out.println("Move 1 from " + cur.from + " to " + cur.to);
} else {
// 如果不只1层
if (!finishLeft.contains(cur)) {
// 如果当前任务没有加入过左子树的任务
// 现在就要加入了!
// 把当前的任务重新压回去,因为还不到打印的时候
// 再加入左子树任务!
finishLeft.add(cur);
stack.push(cur);
stack.push(new Record(cur.level - 1, cur.from, cur.temp, cur.to));
} else {
// 如果当前任务加入过左子树的任务
// 说明此时已经是第二次弹出了!
// 说明左子树的所有打印任务都完成了
// 当前可以打印了!
// 然后加入右子树的任务
// 当前的任务可以永远的丢弃了!
// 因为完成了左子树、打印了自己、加入了右子树
// 再也不用回到这个任务了
System.out.println("Move " + cur.level + " from " + cur.from + " to " + cur.to);
stack.push(new Record(cur.level - 1, cur.temp, cur.to, cur.from));
}
}
}
}
/**
* 递归函数过程封装类
*/
public static class Record {
public int level;
public String from;
public String to;
public String temp;
public Record(int n, String from, String to, String temp) {
this.level = n;
this.from = from;
this.to = to;
this.temp = temp;
}
}
整体代码和测试如下:
java
import java.util.HashSet;
import java.util.Set;
import java.util.Stack;
/**
* 暴力递归尝试一:打印n层汉诺塔从最左边移动到最右边的全部过程
*/
public class RecursiveAttemptQHanoi {
/**
* 方法一:从左到右的暴力递归方法
* 思路:
* 汉诺塔从上往下依次是1到n层,按照规则,大的只能在下面,如果要将整体从左侧移到右侧,就需要先将n-1层以上的从左侧移动到中间,
* 然后将n层移动到右侧,再将n-1层以上的从中间移动到右侧,如此递归下去,就能得到结果。
* 过程:
* 1,把n-1层汉诺塔从最左边移动到中间
* 2,把第n层汉诺塔从最左边移动到最右边
* 3,把n-1层汉诺塔从中间移动到最右边
* 时间复杂度:O(2^n - 1)
*/
public static void hanoi1(int n) {
if (n > 0) {
leftToRight(n);
}
}
/**
* 将n层从左移动到右侧
*/
private static void leftToRight(int n) {
if (n == 1) {
// base case,只有一层,直接移动过去
System.out.println("Move 1 from left to right");
return;
}
// 先将n-1从左侧移动到中间
leftToMid(n - 1);
// 将n从左侧移动到右侧
System.out.println("Move " + n + " from left to right");
// 将n-1从中间移动到右侧
midToRight(n - 1);
}
/**
* 将n层从中间移动到右侧
*/
private static void midToRight(int n) {
if (n == 1) {
System.out.println("Move 1 from mid to right");
return;
}
// 将n-1层从中间移动到左侧
midToLeft(n - 1);
// 将n层从中间移动到右侧
System.out.println("Move " + n + " from mid to right");
// 将n-1层从左侧移动到右侧
leftToRight(n - 1);
}
/**
* 将n层从左侧移动到中间
*/
private static void leftToMid(int n) {
if (n == 1) {
System.out.println("Move 1 from left to mid");
return;
}
// 将n-1层从左侧移动到右侧
leftToRight(n - 1);
// 将n层从左侧移动到中间
System.out.println("Move " + n + " from left to mid");
// 将n-1层从右侧移动到中间
rightToMid(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 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 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);
}
/**
* 方法二:在方法一的基础上简化递归函数
* 思路:
* 汉诺塔问题中,一共有三个位置,分别是左侧、中间、右侧,在每一次移动的过程中,一个是源位置,一个是目标位置,剩下的一个就是中间的临时过度的位置。
* 在移动的过程中,如果想把n层从源位置移动到目标位置,就要先将n-1层从源移动到临时过渡位置,然后将n层从原位置移到到目标位置,最后将原来移动到临时位置的从临时位置移动到目标位置。
* 在这个递归过程中,三个位置的角色是相互变化的,可以写成一个函数。
* 在方法一中,将移动的方向写成了函数,导致函数特别多,其实我们可以将方向用参数传递,
* 用from表示n层的起始位置,用to表示要到达的位置,temp表示临时可用的位置,这样在递归函数中交换不同的位置,就可以将函数抽象成一个。
* 方法一和方法二的复杂度是一样的,只是code的代码量不同。
*/
public static void hanoi2(int n) {
if (n > 0) {
hanoiProcess(n, "left", "right", "mid");
}
}
/**
* 汉诺塔递归函数
*
* @param n :层数编号
* @param from :源位置
* @param to :目标位置
* @param temp :临时过渡位置
*/
private static void hanoiProcess(int n, String from, String to, String temp) {
if (n == 1) {
System.out.println("Move 1 from " + from + " to " + to);
return;
}
// 先将n-1从from移动到temp,中间位置就是to
hanoiProcess(n - 1, from, temp, to);
// 再将n从from移动到to
System.out.println("Move " + n + " from " + from + " to " + to);
// 最后将n-1从temp移动到to,中间位置就是from
hanoiProcess(n - 1, temp, to, from);
}
/**
* 方法三:汉诺塔的非递归实现
* 思路:
* 要将递归函数改成非递归,就是要将打印的过程记录下来,然后用栈模拟递归的过程。
* 我们可以将打印的过程封装成一个Record对象,然后依次将这些对象压入栈中,模拟递归的过程。
* 在这个过程中,还要判断是不是最后一层,就是base case的情况,这个过程比较复杂,具体思路如下:
* 把汉诺塔问题想象成二叉树
* 比如当前还剩i层,其实打印这个过程就是:
* 1) 去打印第一部分 -> 左子树
* 2) 打印当前的动作 -> 当前节点
* 3) 去打印第二部分 -> 右子树
* 那么你只需要记录每一个任务 : 有没有加入过左子树的任务
* 就可以完成迭代对递归的替代了
*/
public static void hanoi3(int N) {
if (N < 1) {
return;
}
// 定义一个记录的栈
Stack<Record> stack = new Stack<>();
// 记录每一个记录有没有加入过左子树的任务
Set<Record> finishLeft = new HashSet<>();
// 初始的任务,认为是种子
stack.add(new Record(N, "left", "right", "mid"));
while (!stack.isEmpty()) {
// 弹出当前任务
Record cur = stack.pop();
if (cur.level == 1) {
// 如果层数只剩1了
// 直接打印
System.out.println("Move 1 from " + cur.from + " to " + cur.to);
} else {
// 如果不只1层
if (!finishLeft.contains(cur)) {
// 如果当前任务没有加入过左子树的任务
// 现在就要加入了!
// 把当前的任务重新压回去,因为还不到打印的时候
// 再加入左子树任务!
finishLeft.add(cur);
stack.push(cur);
stack.push(new Record(cur.level - 1, cur.from, cur.temp, cur.to));
} else {
// 如果当前任务加入过左子树的任务
// 说明此时已经是第二次弹出了!
// 说明左子树的所有打印任务都完成了
// 当前可以打印了!
// 然后加入右子树的任务
// 当前的任务可以永远的丢弃了!
// 因为完成了左子树、打印了自己、加入了右子树
// 再也不用回到这个任务了
System.out.println("Move " + cur.level + " from " + cur.from + " to " + cur.to);
stack.push(new Record(cur.level - 1, cur.temp, cur.to, cur.from));
}
}
}
}
/**
* 递归函数过程封装类
*/
public static class Record {
public int level;
public String from;
public String to;
public String temp;
public Record(int n, String from, String to, String temp) {
this.level = n;
this.from = from;
this.to = to;
this.temp = temp;
}
}
public static void main(String[] args) {
int n = 3;
hanoi1(n);
System.out.println("============");
hanoi2(n);
System.out.println("============");
hanoi3(n);
}
}
2.2、打印一个字符串的全部子序列
- 暴力递归尝试二:打印一个字符串的全部子序列
- 思路:
- 将一个字符串转成字符数组以后,其子序列就是任意取0-n个字符所组成的序列。
- 这个问题就可以转为对于某个下标的字符,有要这个字符和不要这个字符两种结果,从而递归得到两种情况下不同的组合。
- 对于递归函数的设计,可以传入字符数组,目前处理的字符的下标,还有之前已经处理好的序列以及结果字符串列表
- 如果是最后一个,就直接加入结果集,如果不是,就分成要和不要两种结果,组成已经处理的字符串继续递归。
java
/**
* 暴力递归尝试二:打印一个字符串的全部子序列
* 思路:
* 将一个字符串转成字符数组以后,其子序列就是任意取0-n个字符所组成的序列。
* 这个问题就可以转为对于某个下标的字符,有要这个字符和不要这个字符两种结果,从而递归得到两种情况下不同的组合。
* 对于递归函数的设计,可以传入字符数组,目前处理的字符的下标,还有之前已经处理好的序列以及结果字符串列表
* 如果是最后一个,就直接加入结果集,如果不是,就分成要和不要两种结果,组成已经处理的字符串继续递归。
*/
public static List<String> subsequences(String s) {
char[] str = s.toCharArray();
String path = "";
List<String> ans = new ArrayList<>();
process(str, 0, path, ans);
return ans;
}
/**
* 递归函数
*
* @param strs : 字符结果集
* @param index :当前递归的下标
* @param path : 之前已经处理好的
* @param ans : 结果集
*/
private static void process(char[] strs, int index, String path, List<String> ans) {
// base case 如果是最后一个,加入结果集
if (index == strs.length) {
ans.add(path);
return;
}
// 不要当前的结果
process(strs, index + 1, path, ans);
// 需要当前的结果
process(strs, index + 1, path + String.valueOf(strs[index]), ans);
}
2.3、打印一个字符串的全部子序列,要求不要出现重复字面值的子序列
- 暴力递归尝试三:打印一个字符串的全部子序列,要求不要出现重复字面值的子序列
- 用一个Set来保存最后的结果,就可以达到去重的效果。
java
/**
* 暴力递归尝试三:打印一个字符串的全部子序列,要求不要出现重复字面值的子序列
* 用一个Set来保存最后的结果,就可以达到去重的效果。
*/
public static List<String> subsequencesNoRepeat(String s) {
char[] str = s.toCharArray();
String path = "";
HashSet<String> set = new HashSet<>();
process2(str, 0, path, set);
return new ArrayList<>(set);
}
/**
* 递归函数
*
* @param strs : 字符结果集
* @param index :当前递归的下标
* @param path : 之前已经处理好的
* @param ans : 结果集
*/
private static void process2(char[] strs, int index, String path, Set<String> ans) {
// base case 如果是最后一个,加入结果集
if (index == strs.length) {
ans.add(path);
return;
}
// 不要当前的结果
process2(strs, index + 1, path, ans);
// 需要当前的结果
process2(strs, index + 1, path + String.valueOf(strs[index]), ans);
}
打印子序列的整体代码和测试如下:
java
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class RecursiveAttemptQPrintAllSubsequences {
/**
* 暴力递归尝试二:打印一个字符串的全部子序列
* 思路:
* 将一个字符串转成字符数组以后,其子序列就是任意取0-n个字符所组成的序列。
* 这个问题就可以转为对于某个下标的字符,有要这个字符和不要这个字符两种结果,从而递归得到两种情况下不同的组合。
* 对于递归函数的设计,可以传入字符数组,目前处理的字符的下标,还有之前已经处理好的序列以及结果字符串列表
* 如果是最后一个,就直接加入结果集,如果不是,就分成要和不要两种结果,组成已经处理的字符串继续递归。
*/
public static List<String> subsequences(String s) {
char[] str = s.toCharArray();
String path = "";
List<String> ans = new ArrayList<>();
process(str, 0, path, ans);
return ans;
}
/**
* 递归函数
*
* @param strs : 字符结果集
* @param index :当前递归的下标
* @param path : 之前已经处理好的
* @param ans : 结果集
*/
private static void process(char[] strs, int index, String path, List<String> ans) {
// base case 如果是最后一个,加入结果集
if (index == strs.length) {
ans.add(path);
return;
}
// 不要当前的结果
process(strs, index + 1, path, ans);
// 需要当前的结果
process(strs, index + 1, path + String.valueOf(strs[index]), ans);
}
/**
* 暴力递归尝试三:打印一个字符串的全部子序列,要求不要出现重复字面值的子序列
* 用一个Set来保存最后的结果,就可以达到去重的效果。
*/
public static List<String> subsequencesNoRepeat(String s) {
char[] str = s.toCharArray();
String path = "";
HashSet<String> set = new HashSet<>();
process2(str, 0, path, set);
return new ArrayList<>(set);
}
/**
* 递归函数
*
* @param strs : 字符结果集
* @param index :当前递归的下标
* @param path : 之前已经处理好的
* @param ans : 结果集
*/
private static void process2(char[] strs, int index, String path, Set<String> ans) {
// base case 如果是最后一个,加入结果集
if (index == strs.length) {
ans.add(path);
return;
}
// 不要当前的结果
process2(strs, index + 1, path, ans);
// 需要当前的结果
process2(strs, index + 1, path + String.valueOf(strs[index]), ans);
}
public static void main(String[] args) {
String test = "acccc";
List<String> ans1 = subsequences(test);
List<String> ans2 = subsequencesNoRepeat(test);
for (String str : ans1) {
System.out.println(str);
}
System.out.println("=================");
for (String str : ans2) {
System.out.println(str);
}
System.out.println("=================");
}
}
2.4、打印一个字符串的全部排列
2.4.1、方法一
- 方法一:暴力递归尝试
- 思路:
- 将一个字符串转成字符串数组以后,要得到字符串的全排列,就要从第一个字符开始,任意选一个,在剩下的字符里面任意选一个,直到只剩一个为止,最后得到一个结果。
- 在这种方法中,我们对于特定的一个位置,选了一个字符以后,尝试下一个之前,要记得恢复现场,否则上一个处理的结果会影响下一个递归。
java
/**
* 暴力递归尝试四:打印一个字符串的全部排列
* 方法一:暴力递归尝试
* 思路:
* 将一个字符串转成字符串数组以后,要得到字符串的全排列,就要从第一个字符开始,任意选一个,在剩下的字符里面任意选一个,直到只剩一个为止,最后得到一个结果。
* 在这种方法中,我们对于特定的一个位置,选了一个字符以后,尝试下一个之前,要记得恢复现场,否则上一个处理的结果会影响下一个递归。
*
*/
public static List<String> permutation1(String s) {
List<String> ans = new ArrayList<>();
if (s == null || s.isEmpty()) {
return ans;
}
char[] str = s.toCharArray();
// 将字符串数组转成ArrayList,方便获取和删除某个位置的字符
ArrayList<Character> rest = new ArrayList<Character>();
for (char cha : str) {
rest.add(cha);
}
String path = "";
process1(rest, path, ans);
return ans;
}
/**
* 递归函数
*
* @param rest :剩下多少字符
* @param path : 之前的决定
* @param ans :结果列表
*/
public static void process1(ArrayList<Character> rest, String path, List<String> ans) {
if (rest.isEmpty()) {
ans.add(path);
return;
}
int N = rest.size();
for (int i = 0; i < N; i++) {
char cur = rest.get(i);
rest.remove(i);
process1(rest, path + cur, ans);
// 跑完以后要将当前字符加上,达到恢复现场的效果,否则for循环执行到下一个,rest中前一个删除的会影响下一次的
rest.add(i, cur);
}
}
2.4.2、方法二
- 方法二:暴力递归尝试的优化
- 思路:
- 递归函数最讲究的是参数的设计,不同的参数直接影响到时间和空间的复杂度,也影响到后续能不能改成动态规划的解法。
- 方法一中是将字符串数组转成ArrayList,方便获取和删除某个位置的字符,这个过程实际上是效率很低的,
- 但是我们也是需要处理某个字符的时候,后续就不需要处理这个字符的操作。
- 我们可以直接在字符数组上操作,处理完一个字符以后,就将这个字符交换到数组的前面,后面就不在处理这个字符了。
- 这样最后将数组转成字符串,就是一个全排列。每次处理还是要恢复现场
java
/**
* 暴力递归尝试四:打印一个字符串的全部排列
* 方法二:暴力递归尝试的优化
* 思路:
* 递归函数最讲究的是参数的设计,不同的参数直接影响到时间和空间的复杂度,也影响到后续能不能改成动态规划的解法。
* 方法一中是将字符串数组转成ArrayList,方便获取和删除某个位置的字符,这个过程实际上是效率很低的,
* 但是我们也是需要处理某个字符的时候,后续就不需要处理这个字符的操作。
* 我们可以直接在字符数组上操作,处理完一个字符以后,就将这个字符交换到数组的前面,后面就不在处理这个字符了。
* 这样最后将数组转成字符串,就是一个全排列。每次处理还是要恢复现场
*/
public static List<String> permutation2(String s) {
List<String> ans = new ArrayList<>();
if (s == null || s.isEmpty()) {
return ans;
}
char[] str = s.toCharArray();
process2(str, 0, ans);
return ans;
}
public static void process2(char[] strs, int index, List<String> ans) {
if (index == strs.length) {
ans.add(String.valueOf(strs));
return;
}
for (int i = index; i < strs.length; i++) {
// 处理当前的字符,将当前字符交换到index位置,因为下次就处理index+1了,就不会再处理这个字符了
swap(strs, index, i);
process2(strs, index + 1, ans);
// 恢复现场
swap(strs, index, i);
}
}
public static void swap(char[] chs, int i, int j) {
char tmp = chs[i];
chs[i] = chs[j];
chs[j] = tmp;
}
2.5、打印一个字符串的全部排列,要求不要出现重复的排列
- 暴力递归尝试五:打印一个字符串的全部排列,要求不要出现重复的排列
- 思路:
- 可以用set去重结果,也可以用下面的思路在过程中去重,效率更高:
- 重复主要是因为不同的字符出现在不同的位置,要避免重复,只需要判断使用过的字符不在使用就可以。
- 我们可以用一个256长度的boolean数组,用来判断欺负对应的下标位置是不是已经被使用过,如果使用过,就不再重复使用,就可以达到去重的效果。
java
/**
* 暴力递归尝试五:打印一个字符串的全部排列,要求不要出现重复的排列
* 思路:
* 可以用set去重结果,也可以用下面的思路在过程中去重,效率更高:
* 重复主要是因为不同的字符出现在不同的位置,要避免重复,只需要判断使用过的字符不在使用就可以。
* 我们可以用一个256长度的boolean数组,用来判断欺负对应的下标位置是不是已经被使用过,如果使用过,就不再重复使用,就可以达到去重的效果。
*/
public static List<String> permutation3(String s) {
List<String> ans = new ArrayList<>();
if (s == null || s.isEmpty()) {
return ans;
}
char[] str = s.toCharArray();
process3(str, 0, ans);
return ans;
}
public static void process3(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, index, i);
process3(str, index + 1, ans);
swap(str, index, i);
}
}
}
}
public static void swap(char[] chs, int i, int j) {
char tmp = chs[i];
chs[i] = chs[j];
chs[j] = tmp;
}
字符全排列的整体代码和测试如下:
java
import java.util.ArrayList;
import java.util.List;
public class RecursiveAttemptQPrintAllPermutations {
/**
* 暴力递归尝试四:打印一个字符串的全部排列
* 方法一:暴力递归尝试
* 思路:
* 将一个字符串转成字符串数组以后,要得到字符串的全排列,就要从第一个字符开始,任意选一个,在剩下的字符里面任意选一个,直到只剩一个为止,最后得到一个结果。
* 在这种方法中,我们对于特定的一个位置,选了一个字符以后,尝试下一个之前,要记得恢复现场,否则上一个处理的结果会影响下一个递归。
*
*/
public static List<String> permutation1(String s) {
List<String> ans = new ArrayList<>();
if (s == null || s.isEmpty()) {
return ans;
}
char[] str = s.toCharArray();
// 将字符串数组转成ArrayList,方便获取和删除某个位置的字符
ArrayList<Character> rest = new ArrayList<Character>();
for (char cha : str) {
rest.add(cha);
}
String path = "";
process1(rest, path, ans);
return ans;
}
/**
* 递归函数
*
* @param rest :剩下多少字符
* @param path : 之前的决定
* @param ans :结果列表
*/
public static void process1(ArrayList<Character> rest, String path, List<String> ans) {
if (rest.isEmpty()) {
ans.add(path);
return;
}
int N = rest.size();
for (int i = 0; i < N; i++) {
char cur = rest.get(i);
rest.remove(i);
process1(rest, path + cur, ans);
// 跑完以后要将当前字符加上,达到恢复现场的效果,否则for循环执行到下一个,rest中前一个删除的会影响下一次的
rest.add(i, cur);
}
}
/**
* 暴力递归尝试四:打印一个字符串的全部排列
* 方法二:暴力递归尝试的优化
* 思路:
* 递归函数最讲究的是参数的设计,不同的参数直接影响到时间和空间的复杂度,也影响到后续能不能改成动态规划的解法。
* 方法一中是将字符串数组转成ArrayList,方便获取和删除某个位置的字符,这个过程实际上是效率很低的,
* 但是我们也是需要处理某个字符的时候,后续就不需要处理这个字符的操作。
* 我们可以直接在字符数组上操作,处理完一个字符以后,就将这个字符交换到数组的前面,后面就不在处理这个字符了。
* 这样最后将数组转成字符串,就是一个全排列。每次处理还是要恢复现场
*/
public static List<String> permutation2(String s) {
List<String> ans = new ArrayList<>();
if (s == null || s.isEmpty()) {
return ans;
}
char[] str = s.toCharArray();
process2(str, 0, ans);
return ans;
}
public static void process2(char[] strs, int index, List<String> ans) {
if (index == strs.length) {
ans.add(String.valueOf(strs));
return;
}
for (int i = index; i < strs.length; i++) {
// 处理当前的字符,将当前字符交换到index位置,因为下次就处理index+1了,就不会再处理这个字符了
swap(strs, index, i);
process2(strs, index + 1, ans);
// 恢复现场
swap(strs, index, i);
}
}
public static void swap(char[] chs, int i, int j) {
char tmp = chs[i];
chs[i] = chs[j];
chs[j] = tmp;
}
/**
* 暴力递归尝试五:打印一个字符串的全部排列,要求不要出现重复的排列
* 思路:
* 可以用set去重结果,也可以用下面的思路在过程中去重,效率更高:
* 重复主要是因为不同的字符出现在不同的位置,要避免重复,只需要判断使用过的字符不在使用就可以。
* 我们可以用一个256长度的boolean数组,用来判断欺负对应的下标位置是不是已经被使用过,如果使用过,就不再重复使用,就可以达到去重的效果。
*/
public static List<String> permutation3(String s) {
List<String> ans = new ArrayList<>();
if (s == null || s.isEmpty()) {
return ans;
}
char[] str = s.toCharArray();
process3(str, 0, ans);
return ans;
}
public static void process3(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, index, i);
process3(str, index + 1, ans);
swap(str, index, i);
}
}
}
}
public static void main(String[] args) {
String s = "acc";
List<String> ans1 = permutation1(s);
for (String str : ans1) {
System.out.println(str);
}
System.out.println("=======");
List<String> ans2 = permutation2(s);
for (String str : ans2) {
System.out.println(str);
}
System.out.println("=======");
List<String> ans3 = permutation3(s);
for (String str : ans3) {
System.out.println(str);
}
}
}
2.6、用递归函数逆序一个栈
暴力递归尝试六:用递归函数逆序一个栈:给你一个栈,请你逆序这个栈,不能申请额外的数据结构, 只能使用递归函数。 如何实现?
- 暴力递归尝试六:用递归函数逆序一个栈
- 思路:
- 栈的作用就是逆序,压入栈中如果要再逆序,就要设计一个函数,每次盗用这个函数的时候,是获取到栈的最后一个元素,而不是弹出第一个,这样弹出的顺序,就是和正常栈逆序的了。
整体代码如下:
java
import java.util.Stack;
/**
* 暴力递归尝试六:用递归函数逆序一个栈
* 给你一个栈,请你逆序这个栈,
* 不能申请额外的数据结构,
* 只能使用递归函数。 如何实现?
*/
public class RecursiveAttemptQReverseStackUsingRecursive {
/**
* 暴力递归尝试六:用递归函数逆序一个栈
* 思路:
* 栈的作用就是逆序,压入栈中如果要再逆序,就要设计一个函数,每次盗用这个函数的时候,是获取到栈的最后一个元素,而不是弹出第一个,这样弹出的顺序,就是和正常栈逆序的了。
*/
public static void reverse(Stack<Integer> stack) {
if (stack.isEmpty()) {
return;
}
// 获取栈底元素
int i = process(stack);
// 继续逆序剩下的栈
reverse(stack);
// 将栈底元素压栈
stack.push(i);
}
/**
* 递归获取栈底元素:
* 栈底元素移除掉,上面的元素盖下来,返回移除掉的栈底元素
*/
public static int process(Stack<Integer> stack) {
// 先弹出一个元素
int result = stack.pop();
// 弹出以后,栈空了,说明result是最后一个元素,直接返回
if (stack.isEmpty()) {
return result;
} else {
// 没有空,说明还有元素,递归调用,直到获取到最后一个元素
int last = process(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());
}
}
}
后记
个人学习总结笔记,不能保证非常详细,轻喷