【算法笔记】暴力递归尝试
递归思想非常重要,计算机编程中很多算法都是要用到递归完成,同时递归也是解决很多问题的思路,比如动态规划,就是在递归的基础上,缓存结果,达到阶梯的目的。本文总结了一下递归尝试的常用题目和思路,方便深入的理解递归。
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)
/*** 方法一:从左到右的暴力递归方法* 思路:* 汉诺塔从上往下依次是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的代码量不同。
/*** 方法二:在方法一的基础上简化递归函数* 思路:* 汉诺塔问题中,一共有三个位置,分别是左侧、中间、右侧,在每一次移动的过程中,一个是源位置,一个是目标位置,剩下的一个就是中间的临时过度的位置。* 在移动的过程中,如果想把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,中间位置就是tohanoiProcess(n - 1, from, temp, to);// 再将n从from移动到toSystem.out.println("Move " + n + " from " + from + " to " + to);// 最后将n-1从temp移动到to,中间位置就是fromhanoiProcess(n - 1, temp, to, from);}
2.1.4、方法三
- 方法三:汉诺塔的非递归实现
- 思路:
- 要将递归函数改成非递归,就是要将打印的过程记录下来,然后用栈模拟递归的过程。
- 我们可以将打印的过程封装成一个Record对象,然后依次将这些对象压入栈中,模拟递归的过程。
- 在这个过程中,还要判断是不是最后一层,就是base case的情况,这个过程比较复杂,具体思路如下:
- 把汉诺塔问题想象成二叉树
- 比如当前还剩i层,其实打印这个过程就是:
-
- 去打印第一部分 -> 左子树
-
- 打印当前的动作 -> 当前节点
-
- 去打印第二部分 -> 右子树
- 那么你只需要记录每一个任务 : 有没有加入过左子树的任务
- 就可以完成迭代对递归的替代了
/*** 方法三:汉诺塔的非递归实现* 思路:* 要将递归函数改成非递归,就是要将打印的过程记录下来,然后用栈模拟递归的过程。* 我们可以将打印的过程封装成一个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;}}
整体代码和测试如下:
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,中间位置就是tohanoiProcess(n - 1, from, temp, to);// 再将n从from移动到toSystem.out.println("Move " + n + " from " + from + " to " + to);// 最后将n-1从temp移动到to,中间位置就是fromhanoiProcess(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个字符所组成的序列。
- 这个问题就可以转为对于某个下标的字符,有要这个字符和不要这个字符两种结果,从而递归得到两种情况下不同的组合。
- 对于递归函数的设计,可以传入字符数组,目前处理的字符的下标,还有之前已经处理好的序列以及结果字符串列表
- 如果是最后一个,就直接加入结果集,如果不是,就分成要和不要两种结果,组成已经处理的字符串继续递归。
/*** 暴力递归尝试二:打印一个字符串的全部子序列* 思路:* 将一个字符串转成字符数组以后,其子序列就是任意取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来保存最后的结果,就可以达到去重的效果。
/*** 暴力递归尝试三:打印一个字符串的全部子序列,要求不要出现重复字面值的子序列* 用一个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);}
打印子序列的整体代码和测试如下:
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、方法一
- 方法一:暴力递归尝试
- 思路:
- 将一个字符串转成字符串数组以后,要得到字符串的全排列,就要从第一个字符开始,任意选一个,在剩下的字符里面任意选一个,直到只剩一个为止,最后得到一个结果。
- 在这种方法中,我们对于特定的一个位置,选了一个字符以后,尝试下一个之前,要记得恢复现场,否则上一个处理的结果会影响下一个递归。
/*** 暴力递归尝试四:打印一个字符串的全部排列* 方法一:暴力递归尝试* 思路:* 将一个字符串转成字符串数组以后,要得到字符串的全排列,就要从第一个字符开始,任意选一个,在剩下的字符里面任意选一个,直到只剩一个为止,最后得到一个结果。* 在这种方法中,我们对于特定的一个位置,选了一个字符以后,尝试下一个之前,要记得恢复现场,否则上一个处理的结果会影响下一个递归。**/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,方便获取和删除某个位置的字符,这个过程实际上是效率很低的,
- 但是我们也是需要处理某个字符的时候,后续就不需要处理这个字符的操作。
- 我们可以直接在字符数组上操作,处理完一个字符以后,就将这个字符交换到数组的前面,后面就不在处理这个字符了。
- 这样最后将数组转成字符串,就是一个全排列。每次处理还是要恢复现场
/*** 暴力递归尝试四:打印一个字符串的全部排列* 方法二:暴力递归尝试的优化* 思路:* 递归函数最讲究的是参数的设计,不同的参数直接影响到时间和空间的复杂度,也影响到后续能不能改成动态规划的解法。* 方法一中是将字符串数组转成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数组,用来判断欺负对应的下标位置是不是已经被使用过,如果使用过,就不再重复使用,就可以达到去重的效果。
/*** 暴力递归尝试五:打印一个字符串的全部排列,要求不要出现重复的排列* 思路:* 可以用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;}
字符全排列的整体代码和测试如下:
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、用递归函数逆序一个栈
暴力递归尝试六:用递归函数逆序一个栈:给你一个栈,请你逆序这个栈,不能申请额外的数据结构, 只能使用递归函数。 如何实现?
- 暴力递归尝试六:用递归函数逆序一个栈
- 思路:
- 栈的作用就是逆序,压入栈中如果要再逆序,就要设计一个函数,每次盗用这个函数的时候,是获取到栈的最后一个元素,而不是弹出第一个,这样弹出的顺序,就是和正常栈逆序的了。
整体代码如下:
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());}}
}
后记
个人学习总结笔记,不能保证非常详细,轻喷