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

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

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());
        }

    }
}

后记

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

相关推荐
油泼辣子多加4 小时前
【实战】自然语言处理--长文本分类(1)DPCNN算法
算法·自然语言处理·分类
Nobody_Cares4 小时前
JWT令牌
java
沐浴露z4 小时前
Kafka入门:基础架构讲解,安装与使用
java·分布式·kafka
神秘的土鸡4 小时前
从数据仓库到数据中台再到数据飞轮:我的数据技术成长之路
java·服务器·aigc·数据库架构·1024程序员节
I'm a winner4 小时前
基于YOLO算法的医疗应用专题:第一章 计算机视觉与深度学习概述
算法·yolo·计算机视觉
vir025 小时前
P1928 外星密码(dfs)
java·数据结构·算法·深度优先·1024程序员节
摇滚侠5 小时前
全面掌握PostgreSQL关系型数据库,备份和恢复,笔记46和笔记47
java·数据库·笔记·postgresql·1024程序员节
喜欢吃燃面5 小时前
数据结构算法题:list
开发语言·c++·学习·算法·1024程序员节
寂静山林5 小时前
UVa 12991 Game Rooms
算法·1024程序员节