记一道普通的迭代题

144 阅读5分钟

题目

Recursively to return all the abbreviations of "Make America Great Again"

字符串:Make America Great Again

缩写示例:MAGA MMGA ... EAGA EMGA...

思路历程: 示例缩写的前两位单词规律显而易见,虽然题目让返回所有的缩写,但后两位示例都GA,难免思路打岔怀疑是否只需要前两个单词。但 all 就是 all ,这里确实要返回全部。

解题思路

暴力解

啪的一声就开始暴力破解,很快啊:四个嵌套for循环

public static void main(String[] args) {
    String hk = "Make";
    String sh = "America";
    String bk = "Great";
    String br = "Again";

    char[] hkArry = hk.toUpperCase().toCharArray();
    char[] shArry = sh.toUpperCase().toCharArray();
    char[] bkArry = bk.toUpperCase().toCharArray();
    char[] brArry = br.toUpperCase().toCharArray();
    int count = 0;
    for(int i=0;i<brArry.length;i++){
        for(int i2=0;i2<bkArry.length;i2++){
            for(int i3=0;i3<shArry.length;i3++){
                for(int i4=0;i4<hkArry.length;i4++){
                    System.out.println(String.valueOf(hkArry[i4])+String.valueOf(shArry[i3])+String.valueOf(bkArry[i2])+String.valueOf(brArry[i]));
                    count++;
                }
            }
        }
    }
    System.out.println(count);
}

以上代码踩了几个坑:

  1. 拼接四个 char 数组 arry[i] 的时候直接用 + 号拼接打印出来的是数字,为什么?

    答:因为发生了隐式类型转换char 类型本质是Unicode编码的16位无符号整数,参与+运算时,如果另一侧是String,char 会自动转为字符,但如果另一侧是数字或其他非字符串类型(比如也是 char),就会被隐式转换为int进行其对应编码值的计算。

  2. 如果不用 String.valueOf() ,还有什么方法让它输出字符串。

    答:用Character.toString()或直接带空字符串""拼接。

  3. 如果给到的字符串是一个超长的句子呢?

    答:那就迭代,那就递归。

迭代解

开局即暴力的原因其实也很简单,我写迭代确实会脑子转不过来,所以这个解法是问DS的。

public static void main(String[] args) {
    String hk = "Make";
    String sh = "America";
    String bk = "Great";
    String br = "Again";

    char[] hkArry = hk.toUpperCase().toCharArray();
    char[] shArry = sh.toUpperCase().toCharArray();
    char[] bkArry = bk.toUpperCase().toCharArray();
    char[] brArry = br.toUpperCase().toCharArray();

    List<char[]> arrays = new ArrayList<>();
    arrays.add(hkArry);
    arrays.add(shArry);
    arrays.add(bkArry);
    arrays.add(brArry);

    int count = generateCombinations(arrays);
    System.out.println(count);
}

public static int generateCombinations(List<char[]> arrays) {
    int count = 0;
    int[] indices = new int[arrays.size()];

    while (true) {
        //遍历char[]列表,再根据indices数组中的索引位置,从char[]数组中获取对应的字符做拼接
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < arrays.size(); i++) {
            sb.append(arrays.get(i)[indices[i]]);
        }
        System.out.println(sb.toString());
        count++;

        // 移动到下一个字符串组合
        int pos = arrays.size() - 1;
        // pos:索引位置,默认从最后一个数组开始
        // indices[pos]:当前数组的索引位置,默认从0开始
        // 当 pos 不为 0 且 indices[pos](当前数组的索引位置) 大于等于当前数组的长度时,
        // 说明当前数组的索引位置已经到达了数组末尾,需要回溯到上一个数组
        // 回溯时,将当前数组的索引位置重置为0,并继续移动到上一个数组
        while (pos >= 0 && ++indices[pos] >= arrays.get(pos).length) {
            indices[pos] = 0;
            pos--;
        }

        // 检查是否完成所有组合
        if (pos < 0) {
            break;
        }
    }

    return count;
}

迭代的写法用一个整型数组存放了四个字符串数组中当前遍历的字符所在的下标,时间复杂度是 O(N1×N2×N3×N4)(四个数组长度的乘积),空间复杂度是 O(1) (只使用少量索引变量)。且本身只是单纯的循环,没有额外的函数调用开销,也没有栈溢出的风险。但有被 JIT 优化的风险(因为是频繁执行的循环体),且嵌套循环比较难维护 (容易看不懂)

JIT(即时编译器)是JVM的内置功能,默认情况下会自动启用。即使不配置任何启动参数,JVM也会在运行时对热点代码进行动态编译和优化。 参数如-XX:+TieredCompilation-XX:CompileThreshold等,可以调整JIT编译器的行为,例如启用分层编译、设置编译阈值等。这些参数并非必须,但可以通过调优进一步提升性能。

递归解

    public static void main(String[] args) {
        String hk = "Make";
        String sh = "America";
        String bk = "Great";
        String br = "Again";

        char[] hkArry = hk.toUpperCase().toCharArray();
        char[] shArry = sh.toUpperCase().toCharArray();
        char[] bkArry = bk.toUpperCase().toCharArray();
        char[] brArry = br.toUpperCase().toCharArray();

        // 将所有数组存入一个二维数组中,便于递归处理
        char[][] allArrays = {hkArry, shArry, bkArry, brArry};

        // 计数器
        int[] count = {0};

        // 开始递归生成组合
        generateCombinations(allArrays, 0, new StringBuilder(), count);

        System.out.println("Total combinations: " + count[0]);
    }

    /**
     * 递归生成所有组合
     * @param arrays 所有字符数组的二维数组
     * @param currentDepth 当前处理的数组层级
     * @param currentCombination 当前已构建的组合字符串
     * @param count 组合计数器
     */
    private static void generateCombinations(char[][] arrays, int currentDepth, StringBuilder currentCombination, int[] count) {
        // 递归终止条件:已处理完所有数组
        if (currentDepth == arrays.length) {
            System.out.println(currentCombination.toString());
            count[0]++;
            return;
        }

        // 遍历当前层级的字符数组
        for (int i = 0; i < arrays[currentDepth].length; i++) {
            // 添加当前字符到组合中
            currentCombination.append(arrays[currentDepth][i]);

            // 递归处理下一层级
            generateCombinations(arrays, currentDepth + 1, currentCombination, count);

            // 回溯:移除当前字符,准备尝试下一个字符
            currentCombination.deleteCharAt(currentCombination.length() - 1);
        }
    }

递归的时间复杂度也是O(N1×N2×N3×N4)(四个数组长度的乘积),但空间复杂度却是 O(n) ,n取决于数组层度,因为每次递归都会创建栈帧,有参数传递的额外开销。且因为Java不支持尾递归优化,其在JVM中所得到的性能优化较低,如果递归层数过深,数组数量极大,可能造成栈溢出。但好处是递归写法够直观,看得懂。

什么是尾递归: 一种减少递归过程中产生的栈帧数的优化算法,其中递归调用是函数的最后一个操作,并且递归调用的结果直接作为当前函数的返回值。

总结

得背思路。。确实自己写不来。。