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

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

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层,其实打印这个过程就是:
    1. 去打印第一部分 -> 左子树
    1. 打印当前的动作 -> 当前节点
    1. 去打印第二部分 -> 右子树
  • 那么你只需要记录每一个任务 : 有没有加入过左子树的任务
  • 就可以完成迭代对递归的替代了
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());
        }

    }
}

后记

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

相关推荐
To_OC5 小时前
LC 51 N 皇后:我以为难的是回溯,结果栽在了对角线下标
javascript·算法·leetcode
腻害兔5 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
2401_841495646 小时前
【操作系统】进程同步与互斥实验报告
c++·算法·操作系统·进程·并发·同步·互斥
码智社6 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
克里斯蒂亚诺更新6 小时前
ruoyi角色权限的对应关系
笔记
萧瑟余晖7 小时前
JDK 26 新特性详解
java·开发语言
马优晨7 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
a1117769 小时前
2FA 验证码生成器(github登录验证 app)
笔记·学习
爱刷碗的苏泓舒10 小时前
PPP-AR 中的参考星选取:数学原理、评价指标与切换处理
算法·gnss·模糊度固定·ppp-ar·星间单差·参考星·卫星端偏差