【问题标题】:Java Choosing a recursive functionJava 选择递归函数
【发布时间】:2012-10-26 15:25:17
【问题描述】:

我在这里有两个不同的递归函数用于在 Java 中反转字符串:

    Long ms1 = System.currentTimeMillis();
    String str1 = reverse1(str);
    ms1 = System.currentTimeMillis() - ms1;

    Long ms2 = System.currentTimeMillis();
    String str2 = reverse2(str);
    ms2 = System.currentTimeMillis() - ms2;

    System.out.println("Input: " + str);
    System.out.println("  Length: " + str.length());
    System.out.println("Reverse 1:");
    System.out.println("  " + herp + " function calls");
    System.out.println("  " + ms1 +  " milliseconds");
    System.out.println("Reverse 2:");
    System.out.println("  " + derp + " function calls");
    System.out.println("  " + ms2 +  " milliseconds");
}
public static String reverse1(String str){
    herp++;
    if(str.length() == 1) return str;
    return reverse1(str.substring(str.length()/2)) + reverse1(str.substring(0, str.length()/2));
}
public static String reverse2(String str){
    derp++;
    if(str.length() == 1) return str;
    return reverse2(str.substring(1)) + str.charAt(0);
}

给定一个长度为 5000 的字符串,这是程序的输出:

Input: ...
  Length: 5000
Reverse 1:
  9999 function calls
  16 milliseconds
Reverse 2:
  5000 function calls
  52 milliseconds

现在为什么具有两倍函数调用的函数要快 3 倍?我应该如何构建我的递归函数以在 Java 中实现最大速度?

【问题讨论】:

  • 您确定没有除法舍入错误吗?
  • 两个反向函数都正确地反转输入字符串,如果这是你的问题
  • 出于好奇,您是否在进行这些测试之前预热了 JVM?虽然如果它没有预热,我预计第一次测试的时间会比第二次要慢。
  • 我的猜测是,这里的大部分时间实际上都花在创建(和存储)字符串上:第一个函数更好,因为它的参数长度迅速下降(5000 - 2500 - 1250 ...),在第二个函数中是 (5000 - 4999 - 4998...)。
  • 需要说明的是:在变量默认可变的语言中,递归与速度无关。这是关于简单的。如果您担心性能而不是简单性,尤其是在像 Java 这样的语言中,您应该意识到迭代解决方案几乎每次都会更快。在这种情况下,迭代 reverse() 可能是 O(N)。

标签: java string recursion


【解决方案1】:

@HenningMakholm 的回答非常好,但我只是想根据@cHao 对迭代方法和不可变字符串的评论将其抛出。这可能最适合发表评论,但我想要答案的空间房地产......

public static String reverse3(String str){
    StringBuilder sb = new StringBuilder();
    int i;
    for(i = str.length() - 1; i >= 0; i--) {
        sb.append(str.charAt(i));
    }
    return sb.toString();
}

这种迭代方法最后只创建一个不可变的String 对象,并在O(n) 时间运行,产生以下结果:

  Length: 5406
Reverse 1:
  10811 function calls
  59 milliseconds
  5406 length correctness test
Reverse 2:
  5406 function calls
  126 milliseconds
  5406 length correctness test
Reverse 3:
  3 milliseconds
  5406 length correctness test

【讨论】:

  • 我正在考虑一个 char 数组,考虑到我们已经确切地知道它有多大。但这也有效...... :) 并演示了迭代(特别是对于有利于它的问题)如何使递归看起来非常缓慢。
  • 是的,我使用 char 数组编写了一个迭代 reverse3,它轻松胜过其他两个。
  • 我认为这也说明了如果可能的话,在进程的最后只创建一个 String 对象是多么重要。这显然不仅仅是 O(n log n) 与 O(n)... 消除不必要的 Object 创建和清理是原因。
【解决方案2】:

这是一个很好的旧算法分析问题。你的 reverse1 应该在 O(n logn) 时间内运行,而 reverse2 需要 O(n²) 时间,所以要反转的字符串越长,reverse2 的速度就越快,reverse1 会更快。

资源使用量不是由调用次数决定的,而是由将字符复制到在每个字符串连接操作中创建的新 String 对象所花费的时间决定的。 reverse2 中的字符串连接平均适用于比reverse1 中的字符串更长的字符串,因此它的总执行时间更长。

  • 在reverse1 中,每个字符被复制 log2(n) 次(其中 n 是原始字符串的长度),因为递归调用树的深度约为 log2(n)。

  • 在reverse2 中,每个字符被复制的次数等于它在原始字符串中的位置(±1,我不关心)。平均每个字符生成 n/2 个副本。

对于较大的 n,log2(n) 远小于 n/2,因此reverse1 往往更快。

【讨论】:

  • 谢谢!这将是一个关于递归和运行时间复杂性的很好的面试问题。
  • @Eric 请注意,这取决于字符串实现。
【解决方案3】:

因为str.length() == 1,大约 50% 的第一种类型的调用没有做任何工作就结束了。这使得具有非平凡工作的调用数量大致相等。如果您将 derp++ 和 herp++ 调用移到退出条件之后,您将获得“非平凡”调用的数量,它们将相等。

其他调用也会更快,因为平均而言它们会连接较短的字符串,从而弥补 3 倍的差异。

【讨论】:

    猜你喜欢
    • 2017-07-01
    • 1970-01-01
    • 2015-07-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-11-26
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多