算法26:暴力递归

目录

题目1:给你一个字符串,要求打印打印出这个字符串的全部子序列(子序列不能重复)

题目2:打印一个字符串的全部排列。

题目3:针对题目2,要求去除重复元素

题目4:给定一个字符串,要求打印这个字符串中字符能够能够组成的全部子集,子集无重复值。

题目5:给你一个栈,请你逆序这个栈,不能申请额外的数据结构, 只能使用递归函数。 


递归是一个过渡,为动态规划做准备。递归写好了,可以大量的优化代码结构,但是缺点是逻辑不好懂。一个好的递归,参数设计尤为重要。

题目1:给你一个字符串,要求打印打印出这个字符串的全部子序列(子序列不能重复)

这一题需要注意的是,给定了一个字符串,而字符串肯定是有顺序的,比如字符串为abc, 那么他的子序肯定不会出现cba。因为这样的顺序完全被颠倒了。所有,子序是不能改变原字符串的整体顺序的,在保持整体顺序的情况下自由组合。

package code03.递归_06;

import java.util.ArrayList;
import java.util.List;

/**
 * 给你一个字符串,要求打印打印出这个字符串的全部子序列(子序列不能重复)
 */
public class StringSequence_02
{

    public static List<String> getChildSuquence(String str)
    {
        char[]  chars = str.toCharArray();
        List<String> result = new ArrayList<>();
        //path这个参数很重要,它就是一个逐层拼接完整的字符串
        //根据递归遍历的层数,组成不同的字符串,非常灵巧
        String path = "";
        func(chars, result, 0, path);
        return result;
    }

    /**
     * 字符串是有序的,所以子序列必须要按照字符串的整体序列进行。 比如abc, 子序列肯定不能
     * 出现ba或者ca这种情况。
     *
     * 当前递归的逻辑是从后往前,就是找到最后一个元素。 下标依次是:
     *  N-2... N
     *  N-3...N
     *  N-4...N
     *  0...N
     *  N代表的是数组最后一个元素的下标。 而 N+1 就是代表的是数组的长度,
     *  此时,已经没有对应的下标了,所以认为是出口
     *
     *  假设字符串为abc, 那么它的子集为
     *  1.空字符串
     *  2.c
     *  3.b
     *  4.bc
     *  5.a
     *  6. 52组合得到ac,53z组合河道ab,54组合得到abc
     */
    static void func (char[] chars, List<String> list, int index, String path)
    {
        /**
         * 递归的出口。
         * index是最后一个字符的下标。而index+1就是数组的长度
         * 所以就需要跳出递归了
         */
        if (index == chars.length) {
            //排除重复元素
            if (!list.contains(path)) {
                list.add(path);
            }
            return;
        }
        //一直往下找,直到数组的尽头,默认给个空字符串
        func(chars, list, index + 1, path); //此处index+1用的很灵巧,因为index值本身是没有改变的

        //此处会改变path的值,用的很灵巧
        //每一个方法对弈一个方法栈,而变量path存在本地变量表,方法结束,本地变量表销毁
        //因此,此处改变的path只是在当前方法中生效。不会影响其他方法中相同名称的path值
        path = path + String.valueOf(chars[index]);

        //此处开始根据路径进行拼接。
        func(chars, list, index + 1, path);
    }

    public static void main(String[] args) {
        String test = "abc";
        //String test = "abcc";
        List<String>  result = getChildSuquence(test);

        for (String str : result) {
            System.out.println(str);
        }
    }
}

这一题解释一下:

设计思路是根据数组的下标索引进行的,而递归是一直找到下标最后一个元素为止。

此时,如果最后一个元素为c。如果c不被包括,那么path为空字符串,func搜集到空字符串。如果c被包括,那就 func 搜集到的就是 空字符串与c的拼接。 即c。递归结束,返回上一层

此时,返回到元素b处,此时不包括b的元素搜集完毕。即搜集到空字符串和c。path重新组合。 此时path为空字符串和b的组合。再次func操作。而func内部又分为不包含path和包含path两种逻辑,此时针对的是c。如果不包含c,那就搜集到b。如果包含c,那就收集到了b和c的组合,即bc

以此类推.......

整个流程图如下:

搜集结果为: 空字符串、c、b、bc、a、ac、ab、abc 

题目2:打印一个字符串的全部排列

这个字符串的排序指的是可以任意变换字符串的顺序,而不是子字符串。 比如: abc 其中一个就是cba 但是ab a ac等都不属于全部排序

package code03.递归_06;

import java.util.ArrayList;
import java.util.List;

/**
 * 给定一个字符串,要求打印这个字符串的全部排序。
 * 注意:这个字符串的排序指的是可以任意变换字符串的顺序,而不是子字符串
 * 比如: abc 其中一个就是cba   但是ab a ac等都不属于全部排序
 */
public class PrintAllFullStr_03
{
    public static List<String> getAllStr(String str)
    {
        List<String> result = new ArrayList<>();
        String path = "";
        char[] chars = str.toCharArray();
        List<Character> list = new ArrayList<>();
        for (char cha : chars) {
            list.add(cha);
        }
        func(list, result, path);
        return result;
    }

    static void func (List<Character> chars, List<String> result, String path)
    {
        if (chars.isEmpty()) {
            result.add(path);
        }
        else {
            for (int i = 0; i < chars.size(); i++) {
                char cur = chars.get(i);
                //清除现场
                chars.remove(i);
                func(chars, result,  path + cur);
                //恢复现场
                chars.add(i, cur);
            }
        }
    }

    public static void main(String[] args) {
        String test = "abc";
        //String test = "acc";
        List<String> result = getAllStr(test);
        for (String str : result) {
            System.out.println(str);
        }
    }
}

其实,这一道题,还有另一种比较通用的解法,思路与题目1基本一样。

package code03.递归_06;

import java.util.ArrayList;
import java.util.List;

/**
 * 给定一个字符串,要求打印这个字符串的全部排序。
 * 注意:这个字符串的排序指的是可以任意变换字符串的顺序,而不是子字符串
 * 比如: abc 其中一个就是cba   但是ab a ac等都不属于全部排序
 */
public class PrintAllFullStr_03_opt
{
    public static List<String> getAllStr(String str)
    {
        char[] chars = str.toCharArray();
        List<String> result = new ArrayList<>();
        String path = "";
        func(chars, result, 0, path);
        return result;
    }

    static void func (char[]  chars, List<String> result, int index, String path)
    {
        if (index == chars.length) {
            result.add(path);
        }
        else {
            for (int i = index; i < chars.length; i++) {
                swap(chars, index, i);
                func(chars, result, index + 1, path + chars[index]);
                swap(chars, index, i);
            }
        }
    }

    public static void swap(char[] chs, int i, int j) {
        char tmp = chs[i];
        chs[i] = chs[j];
        chs[j] = tmp;
    }

    public static void main(String[] args) {
        String test = "abc";
        //String test = "acc";
        List<String> result = getAllStr(test);
        for (String str : result) {
            System.out.println(str);
        }
    }
}

针对通用解法,我们发现,既然chs始终是动态排序的,但是内部元素并不会减少。因此,可以稍微优化一下参数:

package code03.递归_06;

import java.util.ArrayList;
import java.util.List;

/**
 * 给定一个字符串,要求打印这个字符串的全部排序。
 * 注意:这个字符串的排序指的是可以任意变换字符串的顺序,而不是子字符串
 * 比如: abc 其中一个就是cba   但是ab a ac等都不属于全部排序
 */
public class PrintAllFullStr_03_opt2
{
    public static List<String> getAllStr(String str)
    {
        char[] chars = str.toCharArray();
        List<String> result = new ArrayList<>();
        func(chars, result, 0);
        return result;
    }

    static void func (char[]  chars, List<String> result, int index)
    {
        if (index == chars.length) {
            result.add(String.valueOf(chars));
        }
        else {
            for (int i = index; i < chars.length; i++) {
                swap(chars, index, i);
                func(chars, result, index + 1);
                swap(chars, index, i);
            }
        }
    }

    public static void swap(char[] chs, int i, int j) {
        char tmp = chs[i];
        chs[i] = chs[j];
        chs[j] = tmp;
    }

    public static void main(String[] args) {
        String test = "abc";
        //String test = "acc";
        List<String> result = getAllStr(test);
        for (String str : result) {
            System.out.println(str);
        }
    }
}

其实,这一道题的解题思路就是逐步确定每个位置,而记录位置的参数是index。然后for循环,把剩余的数组元素,依次填补当前位置。以此类推,典型的深度优先遍历算法。流程图如下:

题目3:针对题目2,要求去除重复元素

 去除重复元素,其实分为结果过滤和条件过滤。结果过滤就是走完所有的流程,到最后搜集结果的时候通过结果集去重。而条件过滤则是从源头排除,性能更高。以题目2为例。如果字符串为acc. 下标为1的时候,c出现一次,下一个c再次出现的时候,直接排除掉。

结果过滤:

package code03.递归_06;

import java.util.ArrayList;
import java.util.List;

/**
 * 给定一个字符串,要求打印这个字符串的全部排序, 并且
 * 这些排完序的字符串无重复数据
 *
 * 结果过滤: 执行完流程,待放入集合中时判断是否重复进行过滤。
 * 缺点很明显, 不管是否重复,所有流程都会走一遍,性能低
 *
 */
public class PrintAllFullStrNoRepeat_04
{
    public static List<String> getAllStr(String str)
    {
        char[] chars = str.toCharArray();
        List<String> result = new ArrayList<>();
        String path = "";
        func(chars, result, 0);
        return result;
    }

    static void func (char[]  chars, List<String> result, int index)
    {
        if (index == chars.length) {
            /**
             * 结果过滤,可以直接使用set
             * 此处使用的是list,需要判断集合是否存在重复元素
             */
            if (!result.contains(String.valueOf(chars))) {
                result.add(String.valueOf(chars));
            }
        }
        else {
            /**
             * index代表当前位置开头。
             * 比如index为0, 就是锁定0位置的开头数组。 for循环
             * 是找到 a b c 分别占据0的位置作为开头。 比如 a**. b**, c**
             *
             * 当index为1时,代表分别找到所有数据作为1下标位置开头。 即*a*, *b*, *c*
             *
             */
            for (int i = index; i < chars.length; i++) {
                swap(chars, index, i);
                func(chars, result, index + 1);
                swap(chars, index, i);
            }
        }
    }

    public static void swap(char[] chs, int i, int j) {
        char tmp = chs[i];
        chs[i] = chs[j];
        chs[j] = tmp;
    }

    public static void main(String[] args) {
        //String test = "abc";
        String test = "acc";
        List<String> result = getAllStr(test);
        for (String str : result) {
            System.out.println(str);
        }
    }
}

条件过滤:

package code03.递归_06;

import java.util.ArrayList;
import java.util.List;

/**
 * 给定一个字符串,要求打印这个字符串的全部排序, 并且
 * 这些排完序的字符串无重复数据
 *
 * 结果过滤: 执行完流程,待放入集合中时判断是否重复进行过滤。
 * 缺点很明显, 不管是否重复,所有流程都会走一遍,性能低
 *
 */
public class PrintAllFullStrNoRepeat_04_opt
{
    public static List<String> getAllStr(String str)
    {
        char[] chars = str.toCharArray();
        List<String> result = new ArrayList<>();
        String path = "";
        func(chars, result, 0);
        return result;
    }

    static void func (char[]  chars, List<String> result, int index)
    {
        if (index == chars.length) {
            result.add(String.valueOf(chars));
        }
        else {
            //ASCII码的范围是0-255, 它可以表示所有的字符
            //每一次递归都new一个,用的很巧妙。需要仔细揣摩
            boolean[] visited = new boolean[256];
            for (int i = index; i < chars.length; i++) {
                /**
                 * index是当前位置。第一次进入肯定是通过的。占据当前位置
                 *
                 * 原始字符数组为 a b c        原始数组为acc
                 * 1. a b c                   a c c   原始的c c位置没变。 也就是说第二个位置被第一个c占据了一遍
                 * 2. a c b                   a c c 此时的 c c是交换后的位置。也就是第二c想要再次来占据第二个位置作为后续数组的开头,重复占领该位置,直接pass掉
                 * 3. b a c                   c a c
                 * 4. b c a                   c c a
                 * 5. c a b                   c a c  同理
                 * 6  c b a                   c c a  同理
                 */
                if (!visited[chars[i]]) {
                    visited[chars[i]] = true;
                    swap(chars, index, i);
                    func(chars, result, index + 1);
                    swap(chars, index, i);
                }
            }
        }
    }

    public static void swap(char[] chs, int i, int j) {
        char tmp = chs[i];
        chs[i] = chs[j];
        chs[j] = tmp;
    }

    public static void main(String[] args) {
        //String test = "abc";
        String test = "accb";
        List<String> result = getAllStr(test);
        for (String str : result) {
            System.out.println(str);
        }
    }
}

题目4:给定一个字符串,要求打印这个字符串中字符能够能够组成的全部子集,子集无重复值。

弄懂了题目2,这一题其实就很简单了。

package code03.递归_06;

import java.util.ArrayList;
import java.util.List;

/**
 * 给定一个字符串,要求打印这个字符串的全部排序。
 * 注意:这个字符串的排序指的是可以任意变换字符串的顺序,而不是子字符串
 * 比如: abc 其中一个就是cba   但是ab a ac等都不属于全部排序
 */
public class PrintAllStr_05_opt
{
    public static List<String> getAllStr(String str)
    {
        char[] chars = str.toCharArray();
        List<String> result = new ArrayList<>();
        String path = "";
        func(chars, result, 0, path);
        return result;
    }

    static void func (char[]  chars, List<String> result, int index, String path)
    {
        if (index == chars.length) {
            result.add(path);
        }
        else {
            boolean[] visited = new boolean[256];
            for (int i = index; i < chars.length; i++) {
                visited[chars[i]] = true;
                swap(chars, index, i);
                func(chars, result, index + 1, path + chars[index]);
                if (index < chars.length-1) {
                    result.add(path + chars[index]);
                }
                swap(chars, index, i);
            }
        }
    }

    public static void swap(char[] chs, int i, int j) {
        char tmp = chs[i];
        chs[i] = chs[j];
        chs[j] = tmp;
    }

    public static void main(String[] args) {
        String test = "abc";
        //String test = "acc";
        List<String> result = getAllStr(test);
        System.out.println("length is " + result.size());
        for (String str : result) {
            System.out.println(str);
        }
    }
}

题目5:给你一个栈,请你逆序这个栈,不能申请额外的数据结构, 只能使用递归函数。 

package code03.递归_06;

import java.util.Stack;

/**
 * 题目:
 * 给你一个栈,请你逆序这个栈,
 * 不能申请额外的数据结构,
 * 只能使用递归函数。 如何实现?
 */
public class StackReverse_06 {

    public static void reverse (Stack<Integer> stack)
    {
        if (stack == null || stack.isEmpty()) {
            return;
        }
        //获取栈底元素
        int result = func(stack);
        /**
         * 继续逆转栈中剩余的元素
         * 例如栈中从上到下为 3 2 1
         * 第一次 获取result 1
         * 第二次 获取result 2
         * 第三次 获取result 3
         */
        reverse(stack);
        /**
         * 那么最后一次递归结束后,放入元素
         * 顺序是 放入 1 2 3. 顺序变为  1  2 3
         */
        stack.push(result);
    }

    /**
     * 当前递归,每次结束的时候找到的都是栈底元素
     * @param stack
     * @return
     */
    public static int func(Stack<Integer> stack)
    {
        //如果cur是栈底元素,那么当前pop以后栈就空了
        //直接返回cur
        int cur = stack.pop();
        if (stack.isEmpty()) {
            return cur;
        }
        else {
            //栈底元素
            int last = func(stack);
            //倒数第二个放入栈底
            stack.push(cur);
            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);
        System.out.println("逆转前顺序为: 5 4 3 2 1");
        reverse(test);
        System.out.print("逆转后顺序为: ");
        while (!test.isEmpty()) {
            System.out.print(test.pop());
            System.out.print(" ");
        }

    }
}

这一题很难,难在不允许使用其他的结构来辅助。只能在栈中自己折腾。既然是逆序,那么放置的顺序就是要反过来,栈的特性是先进后出,这就复杂了起来,下面看下整个流程:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值