【问题标题】:Recursive String Concatenation递归字符串连接
【发布时间】:2016-09-13 18:13:24
【问题描述】:

我正在研究一个需要递归串联字符串的问题,但遇到了问题。

问题指出s(0) = 0, s(1) = 1, s(n) = s(n-1)s(n-2) for n >= 2,其中s(n) 是前两个字符串的串联字符串。

输入将指示(n, k) 对的多少实例将作为第一个整数输入,然后是包含非负整数的每一行 n (0 <= n <= 60) 和一个正整数 k

输出应该是打印出连接字符串s(n)的第k个字符,其中k小于或等于字符串s(n)中的字符数。

s(0) = 0
s(1) = 1
s(2) = 10
s(3) = 101
s(4) = 10110
s(5) = 10110101
and so on.

示例输入:

3
5 2
0 1
4 3

输出:

0
0
1

我的代码:

import java.util.*;

public class recursivestring {

    public static String recursive(int n, int i, String str1, String str2){

        if (i == n - 1) 
            return str1 + str2; 

        return recursive(n, i + 1 , str1 + str2, str1);

    }
    public static void main(String[] args) {

        int lines, i, n, k;
        String result;
        Scanner input = new Scanner(System.in);

        lines = input.nextInt();

        for (i = 0; i < lines; i++) {
            n = input.nextInt();
            k = input.nextInt();
            if (n == 0) {
                result = "0";
            } else if (n == 1) {
                result = "1";
            } else if (n == 2) {
                result = "10";
            } else  {
                result = recursive(n, 2, "10", "1");
            }
            System.out.println(result.charAt(k-1));
        }
    }
}

这是我目前所拥有的,它适用于给定的示例测试用例。它适用于大多数情况,但是一旦 n 变大,我就会收到此错误

线程“主”java.lang.OutOfMemoryError 中的异常:Java 堆空间

为什么会这样?我的代码有问题吗?

谢谢!

【问题讨论】:

标签: java recursion


【解决方案1】:

您的方法的问题在于它创建了太多的一次性字符串。每次写完

return str1 + str2;

创建了一个新的String 对象。此类丢弃对象的数量随n 线性增长,而它们的总长度增长为 O(n2)。

你有两个解决这个问题的方法:

  • 保持程序线性,并在顶层传递StringBuilder。每次递归调用都会调用append,而不是使用运算符+ 进行连接。
  • 使用Memoization - 因为您需要计算的字符串数量很少,存储您目前计算的字符串并重新使用它们应该可以解决问题。

您的问题限制的一个更大问题是它的输出不适合String:Java 允许长度最多为 231 的字符串,而您的代码输出为 60更长一点 - 即 1,548,008,755,920 个字符。虽然您应该能够将此输出保存在文件中,但无法将其存储为 String,无论是否有记忆。

【讨论】:

  • 我觉得这对原始问题来说是非常重要的一点......尽管在这一点上它并没有真正构成太多的答案。但是,是的,这是一个很好的观点;鉴于您的递归需要如何工作的定义,您不希望通过字符串连接来执行此操作。
  • 我刚刚尝试了StringBuilder,遇到了同样的问题,正如你提到的,输入60会输出太多字符。然而,问题是,输出必须是屏幕的标准输出,因为这是请求的格式。
  • @M.Lee 那么你应该可以这样编码:link.
【解决方案2】:

由于您是在 Java 中而不是在具有尾调用优化的 FP 语言中执行此操作,因此有两件事是错误的:递归和字符串连接。在 Java 中,您将使用可变构建器进行迭代和字符串 building

具体来说,您的递归函数会保留您在获取完整字符串的过程中生成的所有临时字符串。这是 O(n2) 内存使用量。

【讨论】:

    【解决方案3】:

    嗯,老实说,这对我来说看起来像斐波那契,所以我的方法看起来有点像……

    public static void main(String[] args) {
    for (int i = 0; i < 6; i++) {
        System.out.println(stringbonacci(i));
    }
    }
    
    private static String stringbonacci(int i) {
    if (i == 0) {
        return "0";
    }
    if (i == 1) {
        return "1";
    } else {
        return stringbonacci(i - 1) + stringbonacci(i - 2);
    }
    }
    

    结果如下:

    0

    1

    10

    101

    10110

    10110101

    【讨论】:

    • 像其他答案一样的记忆化比递归重新计算结果更可取
    • 不错的技巧....但这意味着我们需要创建查找表...它会越来越大,不是吗...??
    • 是的。这是存储与运行时的权衡。
    【解决方案4】:

    我强烈建议缓存每个方法调用的结果,这样您就不必多次重新计算所有内容。将以少量堆为代价大大提高您的性能。像……这个?

    public class RecursiveString {
        private static final Map<Integer, String> cache = new HashMap<>();
        static {
            cache.put(0, "0");
            cache.put(1, "1");
        }
    
        public static String generateString(Integer i) {
            if (i < 0) return generateString(0); // or something... avoid negatives.
    
            if (cache.get(i) != null) return cache.get(i); // cache hit.
    
            String generated = String.format("%s%s", 
                generateString(i-1), generateString(i-2));
            cache.put(i, generated);
    
            return generated;
        }
    }
    

    请注意,在 2G 堆上,generateString 在我的机器上通过 i=40 工作,这仅仅是因为生成的字符串的长度。字符串的长度将是2*fib(i) 字节大小,因此一旦您开始访问这些索引,您的内存需求就会迅速爆炸。

    【讨论】:

    • String.format 的开销比s1 + s2 多还是少?
    • @cricket_007 减少开销。 String.format 在后台使用 StringBuilder。
    【解决方案5】:

    与地图的答案相同(存储中间字符串,不要重新计算),但改用数组。

    这可以通过读取所有行来进一步优化,然后找到最大的 n 值,从而只存储一个数组,然后循环返回 (n, k) 对。

    import java.util.Scanner;
    
    public class BinaryConcat {
        private String[] dict;
    
        public BinaryConcat(int n) {
            if (n < 0) throw new IllegalArgumentException();
            dict = new String[2 + n]; // Have 2 base cases
    
            dict[0] = "0";
            dict[1] = "1";
        }
    
        public String getValue(int n) {
            if (n < 0) throw new IllegalArgumentException();
            if (n <= 1) return dict[n];
            if (dict[n] == null || dict[n].isEmpty()) {
                dict[n] = String.format("%s%s", getValue(n - 1), getValue(n - 2));
            }
    
            return dict[n];
        }
    
        public static void main(String[] args) {
    
            Scanner input = new Scanner(System.in);
    
            int lines = input.nextInt();
            input.nextLine(); // consume linefeed
    
            for (int i = 0; i < lines; i++) {
                String line = input.nextLine();
                String[] nk = line.split(" ");
                int n = Integer.parseInt(nk[0]);
                int k = Integer.parseInt(nk[1]);
    
                String value = new BinaryConcat(n).getValue(n);
                System.out.println(value.charAt(k - 1));
            }
    
        }
    }
    

    示例运行

    (与预期相同的输入和输出)

    【讨论】:

    • 试过运行这个,仍然是同样的问题,当输入'n'是60这样大的东西时,使用标准System.out的输出字符太多。
    • 我将 40 打印到我的 IDE 控制台,但它确实减慢了速度。我认为 60 的 n 会更糟。可能有一种更聪明的方法来处理某些数字
    • 而更聪明的方法很可能只计算字符串到字符k
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-23
    • 1970-01-01
    • 1970-01-01
    • 2020-07-19
    • 1970-01-01
    • 2013-04-06
    相关资源
    最近更新 更多