【问题标题】:Java Benchmark for recursive stairs climbing puzzle递归楼梯攀爬难题的 Java 基准测试
【发布时间】:2014-10-27 10:42:24
【问题描述】:

监考人员在白板考试期间提出了这个现在非常常见的算法问题。我的工作是观察、倾听和客观地判断给出的答案,但我无法控制这个问题,也无法与回答的人互动。 有五分钟的时间来分析问题,候选人可以写子弹笔记,伪代码(这在实际编写代码时是允许的,只要明确指出,以及包括伪代码作为 cmets 或 TODO 任务的人在弄清楚算法获得奖励积分之前)。

  • “一个孩子正在爬 n 级楼梯,一次可以跳 1 级、2 级或 3 级。实现一种方法来计算孩子可以跳上楼梯的可能方式。”

得到这个问题的人无法当场开始使用递归算法,因此监考人员最终将他逐个引导到他的解决方案,在我看来这不是最佳的(嗯,与我的不同)选择的解决方案使得很难客观地对某人进行代码优化评分)。

监考人员:

public class Staircase {

public static int stairs;

public Staircase() {

    int a = counting(stairs);
    System.out.println(a);

}

static int counting(int n) {
    if (n < 0)
        return 0;
    else if (n == 0)
        return 1;
    else
        return counting(n - 1) + counting(n - 2) + counting(n - 3);
}

public static void main(String[] args) {
    Staircase child;
    long t1 = System.nanoTime();
    for (int i = 0; i < 30; i++) {
        stairs = i;
        child = new Staircase();            
    }
    System.out.println("Time:" + ((System.nanoTime() - t1)/1000000));

}
}

//

我的:

public class Steps {

public static int stairs;
int c2 = 0;

public Steps() {

    int a = step2(0);
    System.out.println(a);

}

public static void main(String[] args) {
    Steps steps;
    long t1 = System.nanoTime();
    for (int i = 0; i < 30; i++) {
        stairs = i;
        steps = new Steps();
    }
    System.out.println("Time:" + ((System.nanoTime() - t1) / 1000000));
}

public int step2(int c) {

    if (c + 1 < stairs) {
        if (c + 2 <= stairs) {
            if (c + 3 <= stairs) {
                step2(c + 3);
            }
            step2(c + 2);
        }
        step2(c + 1);
    } else {
        c2++;
    }
    return c2;
}
}

输出: 监考人员:时间:356 我的:时间:166

有人可以澄清哪种算法更好/更优化吗?我的算法的执行时间似乎不到一半,(但我正在引用和更新一个我认为相当无关紧要的额外整数),它允许设置任意开始和结束步骤,而无需首先现在他们的区别(虽然对于任何高于 n=40 的东西,你都需要一个 CPU 的野兽)。

我的问题:(请随意忽略上面的示例)您如何正确地对类似的基于递归的问题(河内塔等)进行基准测试。您是只看时间,还是考虑其他因素(堆?)。

【问题讨论】:

  • 没有真正回答你的问题,但据我所知,通过矩阵求幂可以解决这个问题的 O(log n)。类似于斐波那契的矩阵求幂解。

标签: java algorithm recursion


【解决方案1】:

Teaser:您可以在不到一毫秒的时间内轻松执行此计算。详情如下...


哪个“更好”?

哪种算法“更好”的问题可能与执行时间有关,但也与其他事物有关,例如实现风格。

Staircase 实现更短、更简洁,恕我直言,更具可读性。更重要的是:它不涉及状态。您在此处引入的 c2 变量破坏了纯函数递归实现的优点(和美感)。这可能很容易解决,尽管实现已经变得与Staircase 更相似。


衡量绩效

关于执行时间的问题:在 Java 中正确测量执行时间是很棘手的。

相关阅读:

为了正确可靠地测量执行时间,有多种选择。除了像 VisualVM 这样的分析器之外,还有像 JMHCaliper 这样的框架,但不可否认,使用它们可能需要一些努力。

对于非常基本的手动 Java Microbenchmark 的最简单形式,您必须考虑以下几点:

  • 多次运行算法,让 JIT 有机会发挥作用
  • 交替运行算法,而不是一个接一个地运行
  • 随着输入大小的增加运行算法
  • 以某种方式保存并打印计算结果,以防止计算被优化
  • 不要在控制台基准测试期间打印任何内容
  • 考虑到时间可能被垃圾收集器 (GC) 扭曲

再次重申:这些只是经验法则,可能仍会出现意想不到的结果(有关详细信息,请参阅上面的链接)。但是通过这种策略,您通常可以很好地了解性能,并且至少可以看到算法之间是否存在真的显着差异。


方法之间的差异

Staircase 实现和Steps 实现没有太大区别。

主要的概念区别在于Staircase 实现是向下,而Steps 实现是向上

实际影响性能的主要区别在于基本案例的处理方式(参见维基百科上的Recursion)。在您的实现中,您避免在不需要时递归调用该方法,代价是一些额外的if 语句。 Staircase 实现对基本情况进行了非常通用的处理,只需检查 n &lt; 0 是否存在。

可以考虑结合两种方法的想法的“中间”解决方案:

class Staircase2
{
    public static int counting(int n)
    {
        int result = 0;
        if (n >= 1) 
        {
            result += counting(n-1);
            if (n >= 2) 
            {
                result += counting(n-2);
                if (n >= 3) 
                {
                    result += counting(n-3);
                }
            }
        }
        else
        {
            result += 1;
        }
        return result;
    }
}

它仍然是没有状态的递归,并且总结了中间结果,通过使用一些if 查询避免了许多“无用”的调用。它已经明显快于最初的Staircase 实现,但仍然比Steps 实现慢一点。


为什么这两种解决方案都很慢

对于这两种实现,实际上没有什么需要计算的。该方法由少量if 语句和一些附加内容组成。这里最昂贵的东西实际上是递归本身,带有 deeeeply 嵌套调用树。

这就是这里的关键点:这是一个调用 tree。想象一下它为给定数量的步骤计算了什么,作为“伪代码调用层次结构”:

 compute(5)
  compute(4)
   compute(3)
    compute(2)
     compute(1)
      compute(0)
      compute(0)
     compute(1)
      compute(0)
      compute(0)
    compute(2)
     compute(1)
      compute(0)
      compute(0)
     compute(1)
      compute(0)
   compute(3)
    compute(2)
     compute(1)
      compute(0)
      compute(0)
     compute(1)
      compute(0)
      compute(0)
    compute(2)
     compute(1)
      compute(0)
      compute(0)

可以想象,当数字变大时,它会以指数方式增长。所有的结果都被计算了数百、数千或数百万次。这是可以避免的


快速解决方案

使计算更快的关键思想是使用Dynamic Programming。这基本上意味着中间结果被存储以供以后检索,因此不必一次又一次地计算它们。

在这个例子中实现了,也比较了所有方法的执行时间:

import java.util.Arrays;

public class StaircaseSteps
{
    public static void main(String[] args)
    {
        for (int i = 5; i < 33; i++)
        {
            runStaircase(i);
            runSteps(i);
            runDynamic(i);
        }
    }

    private static void runStaircase(int max)
    {
        long before = System.nanoTime();
        long sum = 0;
        for (int i = 0; i < max; i++)
        {
            sum += Staircase.counting(i);
        }
        long after = System.nanoTime();
        System.out.println("Staircase  up to "+max+" gives "+sum+" time "+(after-before)/1e6);
    }

    private static void runSteps(int max)
    {
        long before = System.nanoTime();
        long sum = 0;
        for (int i = 0; i < max; i++)
        {
            sum += Steps.step(i);
        }
        long after = System.nanoTime();
        System.out.println("Steps      up to "+max+" gives "+sum+" time "+(after-before)/1e6);
    }

    private static void runDynamic(int max)
    {
        long before = System.nanoTime();
        long sum = 0;
        for (int i = 0; i < max; i++)
        {
            sum += StaircaseDynamicProgramming.counting(i);
        }
        long after = System.nanoTime();
        System.out.println("Dynamic    up to "+max+" gives "+sum+" time "+(after-before)/1e6);
    }
}

class Staircase
{
    public static int counting(int n)
    {
        if (n < 0)
            return 0;
        else if (n == 0)
            return 1;
        else
            return counting(n - 1) + counting(n - 2) + counting(n - 3);
    }
}




class Steps
{
    static int c2 = 0;
    static int stairs;

    public static int step(int c)
    {
        c2 = 0;
        stairs = c;
        return step2(0);
    }

    private static int step2(int c)
    {
        if (c + 1 < stairs)
        {
            if (c + 2 <= stairs)
            {
                if (c + 3 <= stairs)
                {
                    step2(c + 3);
                }
                step2(c + 2);
            }
            step2(c + 1);
        }
        else
        {
            c2++;
        }
        return c2;
    }
}


class StaircaseDynamicProgramming
{
    public static int counting(int n)
    {
        int results[] = new int[n+1];
        Arrays.fill(results, -1);
        return counting(n, results);
    }

    private static int counting(int n, int results[])
    {
        int result = results[n];
        if (result == -1)
        {
            result = 0;
            if (n >= 1) 
            {
                result += counting(n-1, results);
                if (n >= 2) 
                {
                    result += counting(n-2, results);
                    if (n >= 3) 
                    {
                        result += counting(n-3, results);
                    }
                }
            }
            else
            {
                result += 1;
            }
        }
        results[n] = result;
        return result;
    }
}

我的电脑上的结果如下:

...
Staircase  up to 29 gives 34850335 time 310.672814
Steps      up to 29 gives 34850335 time 112.237711
Dynamic    up to 29 gives 34850335 time 0.089785
Staircase  up to 30 gives 64099760 time 578.072582
Steps      up to 30 gives 64099760 time 204.264142
Dynamic    up to 30 gives 64099760 time 0.091524
Staircase  up to 31 gives 117897840 time 1050.152703
Steps      up to 31 gives 117897840 time 381.293274
Dynamic    up to 31 gives 117897840 time 0.084565
Staircase  up to 32 gives 216847936 time 1929.43348
Steps      up to 32 gives 216847936 time 699.066728
Dynamic    up to 32 gives 216847936 time 0.089089

语句顺序的细微变化(“微优化”)可能会产生很小的影响,或者会产生显着的差异。但是使用完全不同的方法可以产生真正的不同。

【讨论】:

  • 动态编程非常酷的答案!这很有趣,但我想过以这种方式进行,但在实际计算路径时让自己完全扭曲,我的大脑告诉我“不,DP会给你最短的路径,但在这里你需要以各种可能的方式打击每一步。”然后,我考虑使用 n 步映射并将其应用于剩余的楼梯,但集合中的任何内容都不允许用于该部分的问题(因为其他人被要求构建列表、堆栈等),但找不到及时将其存储在数组中的正确方法。
猜你喜欢
  • 2015-10-11
  • 2014-10-20
  • 1970-01-01
  • 2015-02-22
  • 2021-12-12
  • 2020-08-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多