当前位置: 首页 > news >正文

【算法笔记】暴力递归尝试

   递归思想非常重要,计算机编程中很多算法都是要用到递归完成,同时递归也是解决很多问题的思路,比如动态规划,就是在递归的基础上,缓存结果,达到阶梯的目的。本文总结了一下递归尝试的常用题目和思路,方便深入的理解递归。

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层,其实打印这个过程就是:
    1. 去打印第一部分 -> 左子树
    1. 打印当前的动作 -> 当前节点
    1. 去打印第二部分 -> 右子树
  • 那么你只需要记录每一个任务 : 有没有加入过左子树的任务
  • 就可以完成迭代对递归的替代了
    /*** 方法三:汉诺塔的非递归实现* 思路:* 要将递归函数改成非递归,就是要将打印的过程记录下来,然后用栈模拟递归的过程。* 我们可以将打印的过程封装成一个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());}}
}

后记
个人学习总结笔记,不能保证非常详细,轻喷

http://www.dtcms.com/a/512334.html

相关文章:

  • 一次学会二分法——力扣278.第一个错误的版本
  • 数据结构——二十七、十字链表与邻接多重链表(王道408)
  • 网站公司做的网站被攻击苏州网络推广
  • 网站权重能带来什么作用灰大设计导航网
  • i.MX6ULL Linux内核启动流程深度解析
  • Browser-Use 打造可操作浏览器的 AI 智能体
  • php网站开发入门到精通教程好玩的游戏网页
  • 代码仓库码云(gitee)配置环境记录
  • 织梦网站模板陶瓷广州建设行业网站
  • 面试(六)——Java IO 流
  • 怎么做视频网站教程php彩票网站建设教程
  • 大模型(Large Language Model, LLM)——什么是大模型,大模型的基本原理、架构、流程
  • 长春网站建设排名怎样用自己电脑做网站
  • 基于 Redis 的基数统计:高效的大规模去重与计数
  • 机械外贸网站站长网站工具
  • 广州企业建站素材安徽禹尧工程建设有限公司网站
  • MySQL if函数
  • Promise.all怎么用
  • 成都网站建设开发价玉环哪里有做网站
  • 01)mysql数据误删恢复相关-mysql5.7 开启 binlog、设置binlog 保留时间
  • 电力电子技术 第五章——非连续导电模式
  • Django 项目 .gitignore 模板
  • MySQL 中文排序(拼音排序)不生效问题全解析
  • 建站网络公司云南网站备案难吗
  • 深度学习(8)- PyTorch 数据处理与加载
  • JAVA:Spring Boot 集成 Jackson 实现高效 JSON 处理
  • 深度学习之YOLO系列YOLOv4
  • 江西移动网站建站推广外包
  • 张家口网站建设zjktao温州公司网址公司
  • Cef笔记:Cef消息循环的集成