Java Benchmark用于递归楼梯攀爬拼图

时间:2014-09-03 03:17:36

标签: java algorithm recursion

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

  • "一个孩子正在爬楼梯,只有n步,可以一步跳一步,两步或三步。实施一种方法来计算孩子跳楼梯的可能方式。"

得到这个问题的人当然不能开始使用递归算法,所以监考人员最终逐件引导他进入HIS解决方案,在我看来这不是最优的(好吧,不同)从我选择的解决方案中,很难在客观上对代码优化进行评分。)

普罗克特:

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;
}
}

输出: Proctor:时间:356 我的:时间:166

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

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

1 个答案:

答案 0 :(得分:6)

预告:您可以在不到一毫秒的时间内轻松执行此计算。细节如下......


哪一个是&#34;更好&#34;?

哪种算法更好&#34;可以参考执行时间,也可以参考其他事情,比如执行风格。

Staircase实现更短,更简洁,恕我直言更具可读性。更重要的是:它不涉及 。你在那里引入的c2变量破坏了纯函数递归实现的优点(和美)。这可能很容易修复,尽管实现已经变得更加类似于Staircase


测量性能

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

相关阅读:

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

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

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

再次:这些只是经验法则,可能仍有意外结果(有关详细信息,请参阅上面的链接)。但是通过这种策略,您通常可以获得关于性能的良好指示,并且至少可以看出真正是否可能是算法之间的显着差异。


方法之间的差异

Staircase实施和Steps实施并没有太大差异。

主要的概念差异是Staircase实施正在计算向下,而Steps实施正在计算向上

实际影响性能的主要区别在于如何处理基本案例(请参阅维基百科上的Recursion)。在您的实现中,您可以避免在不需要时以递归方式调用该方法,但代价是一些额外的if语句。 Staircase实现仅使用n < 0来对基本情况进行非常通用的处理。

可以考虑一个&#34;中间体&#34;结合两种方法的想法的解决方案:

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;
    }
}

如果没有状态,它仍然是递归的,并总结了中间结果,避免了许多无用的&#34;使用一些if查询进行调用。它已经明显快于原始Staircase实施,但仍然比Steps实施慢一点。


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

对于这两种实现,并没有真正计算任何东西。该方法包含少量if语句和一些新增内容。这里最昂贵的事情实际上是递归本身,使用 deeeeply 嵌套的调用树。

这是关键点:它是一个电话 。想象一下,对于给定数量的步骤计算什么,作为&#34;伪代码调用层次结构&#34;:

 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

语句顺序的微小变化(&#34;微优化&#34;)可能会产生很小的影响,或产生明显的差异。但是使用完全不同的方法可以使真正的区别开来。