您可能会问两个不同的问题:
- 如何衡量 Java 实现的运行时间(基准测试)
- 如何证明算法的渐近运行时间
对于其中的第一个,我不会使用此处发布的解决方案。他们大多不完全正确。 Forst,使用System.nanoTime 可能比使用System.currentTimeMillis 更好。其次,您需要使用 try catch 块。第三,统计你的代码在指标之外多次运行的运行时间,这样你就可以有一个更完整的画面。
多次运行看起来像这样的代码:
long totalTime = 0;
long startTime = System.nanoTime();
try{
//method to test
} finally {
totalTime = System.nanoTime() - startTime;
}
正确进行基准测试很难。例如,您必须让您的代码“预热”几分钟,然后再对其进行测试。尽早并经常进行基准测试,但不要过分相信您的基准。特别是小型微型基准几乎总是以这样或那样的方式存在。
解释问题的第二种方法是关于渐近运行时间。事实是这几乎与 Java 无关,它是一般的计算机科学。这里我们要问的问题是:哪些曲线根据输入大小来描述我们算法的运行时间行为。
首先要理解 Big-Oh 符号。我会尽力而为,但 SO 不支持数学符号。 O(f(n)) 表示一组算法,使得n 趋于无穷大f(n) 在算法运行时间上限的常数因子内。正式地,T(n) 在O(f(n)) 中,如果存在一些常量n0 和一些常量c,那么对于所有n > n0 c*f(n) >= n。大欧米茄是一样的,除了上限,大 Theta f(n) 只是意味着它既大哦f(n) 和大欧米茄f(n)。这并不难。
好吧,它变得有点复杂,因为我们可以讨论不同类型的运行时间,即“平均情况”、最佳情况和最坏情况。例如,normall quicksort 在最坏的情况下是O(n^2),但对于随机列表是O(n log n)。
所以我跳过了T(n) 的含义。基本上它是“滴答声”的数量。一些机器指令(比如从内存中读取)比其他机器指令(比如添加)花费的时间要长得多。但是,只要它们彼此之间只是一个常数因子,我们就可以将它们都视为 big Oh 的目的,因为它只会改变c 的值。
证明渐近界并不难。对于简单的结构化编程问题,您只需数数
public int square(int n){
int sum = 0
for(int i = 0, i < n, i++){
sum += n
}
return sum
}
在本例中,我们各有一条指令:初始化 sum、初始化 i 和返回值。循环发生n 次,每次我们进行比较、加法和增量时。所以我们有 O(square(n)) = O(3 + 3n) 使用 2 的 n0 和 4 的 c,我们可以很容易地证明这是在 O(n) 中。您始终可以通过删除多余的常数项并除以常数倍数来安全地简化大 Oh 表达式。
当您遇到递归函数时,您必须解决递归关系。如果你有像T(n) = 2*T(n/2) + O(1) 这样的函数,你想找到一个封闭形式的解决方案。有时您必须手动或使用计算机代数系统来完成。对于这个例子,使用前向替换,我们可以看到模式(在符号的滥用中)T(1) = O(1), T(2) = O(3), T(4) = O(7), T(8) = (15) 这看起来很像O(2n - 1),以证明这是正确的值:
T(n) = 2*T(n/2) + 1
T(n) = 2*(2(n/2) - 1) + 1
T(n) = 2*(n-1) + 1
T(n) = 2n - 2 + 1
T(n) = 2n - 1
正如我们之前看到的,您可以将O(2n -1) 简化为O(n)
虽然您可以更经常地使用主定理,它是一种数学工具,可以节省您解决此类问题的时间。如果您检查wikipedia,您可以找到主定理,如果您即插即用上面的示例,您会得到相同的答案。
如需了解更多信息,请查看 Levitin 的“算法设计与分析”等算法教科书