以编程方式获得代码的Big-O效率

时间:2009-01-26 18:09:55

标签: algorithm complexity-theory

我想知道是否有任何自动方法来确定(至少大致)给定函数的Big-O时间复杂度?

如果我绘制O(n)函数与O(n lg n)函数,我想我能够在视觉上确定哪个是哪个;我认为必须有一些启发式解决方案,可以自动完成。

有什么想法吗?

编辑:我很高兴找到一个半自动化的解决方案,只是想知道是否有某种方法可以避免进行全手动分析。

18 个答案:

答案 0 :(得分:56)

听起来你要求的是延迟停止问题。即使在理论上,我也不相信这样的事情是可能的。

回答问题“这行代码是否会运行?”在一般情况下,即使不是不可能,也会非常困难。

编辑添加: 虽然一般情况难以处理,但请参阅此处获取部分解决方案:http://research.microsoft.com/apps/pubs/default.aspx?id=104919

另外,有些人表示手工分析是唯一的选择,但我不认为这是看待它的正确方法。即使将人类添加到系统/机器中,棘手的问题仍然是难以处理的。经过进一步的反思,我认为99%的解决方案可能是可行的,甚至可能比人类更好或更好。

答案 1 :(得分:32)

您可以在各种大小的数据集上运行算法,然后您可以使用曲线拟合来得出近似值。 (在大多数情况下,仅查看您创建的曲线可能就足够了,但任何统计包都有曲线拟合)。

请注意,某些算法会显示一个具有较小数据集的形状,但另一个具有较大的数据...而且大的定义仍然有点模糊。这意味着具有良好性能曲线的算法可能具有如此多的实际开销(对于小数据集),它不能像理论上更好的算法那样工作。

代码检查技术而言,都不存在。但是检测代码以运行各种长度并输出一个简单的文件(RunSize RunLength就足够了)应该很容易。生成正确的测试数据可能会更复杂(某些算法对部分有序数据的效果更好/更差,因此您需要生成表示正常用例的数据)。

由于“什么是大”的定义存在问题以及性能依赖于数据这一事实,我发现静态分析往往具有误导性。在优化性能和在两种算法之间进行选择时,真实世界的“橡胶之路”测试是我信任的唯一最终仲裁者。

答案 2 :(得分:9)

简短的回答是这是不可能的,因为常数很重要

例如,我可能会编写一个在O((n^3/k) + n^2)中运行的函数。这简化为O(n ^ 3),因为当n接近无穷大时,n^3项将主导函数,而不管常数k

但是,如果k在上面的示例函数中非常大,那么该函数似乎几乎完全n^2运行,直到n^3项将开始的某个交叉点主宰。因为任何分析工具都不知道常量k,所以不可能知道测试目标函数的数据集有多大。如果k可以任意大,则无法制作测试数据来确定运行时间。

答案 3 :(得分:8)

我很好奇为什么你希望能够做到这一点。根据我的经验,有人说:“我想确定这个算法的运行时复杂性”,他们并没有问他们认为他们在问什么。您最有可能问的是这种算法对于可能的数据的实际性能。计算函数的Big-O具有合理的实用性,但是有很多方面可以改变实际使用的算法的“实际运行时性能”,没有任何东西胜过仪器和测试。

例如,以下算法具有相同的精确Big-O(古怪伪码):

示例a:

huge_two_dimensional_array foo
for i = 0, i < foo[i].length, i++
  for j = 0; j < foo[j].length, j++
    do_something_with foo[i][j]

例子b:

huge_two_dimensional_array foo
for j = 0, j < foo[j].length, j++
  for i = 0; i < foo[i].length, i++
    do_something_with foo[i][j]

再次,完全相同的大O ...但其中一个使用行标准,其中一个使用列标准。事实证明,由于引用的局部性和缓存一致性,您可能有两个完全不同的实际运行时间,特别是取决于数组foo的实际大小。如果算法是内置并发性的软件的一部分,那么它甚至不会触及算法行为的实际性能特征。

不是负面的,但是大O是一个范围狭窄的工具。如果您深入算法分析,或者如果您正在尝试证明关于算法的某些内容,那么它非常有用,但如果您正在进行商业软件开发,那么证据就在布丁中,并且您将继续想要有实际的性能数字来做出明智的决定。

干杯!

答案 4 :(得分:8)

我很惊讶地看到有这么多人试图通过秒表“衡量”复杂性。有几个人给出了正确的答案,但我认为仍然有空间将重点放在家里。

  1. 算法复杂度不是“编程”问题;这是一个“计算机科学”的问题。回答这个问题需要从数学家的角度分析代码,这样计算Big-O复杂性实际上是一种数学证明。它需要非常强烈地理解基本的计算机操作,代数,可能是微积分(限制)和逻辑。没有多少“测试”可以代替该过程。

  2. 暂停问题适用,因此算法的复杂性从根本上说是机器不可判定的。

  3. The limits of automated tools applies,所以有可能编写一个程序来帮助,但它只能帮助计算器帮助一个人的物理作业,或者像重构一样多浏览器有助于重新组织代码库。

  4. 对于任何认真考虑编写此类工具的人,我建议进行以下练习。选择一个相当简单的算法,例如您喜欢的排序,作为主题算法。获得可靠的参考(书籍,基于Web的教程),引导您完成计算算法复杂性的过程,最终获得“Big-O”。在使用主题算法完成整个过程时记录您的步骤和结果。执行这些步骤并记录几种情况下的进度,例如最佳情况,最坏情况和平均情况。完成后,请查看您的文档,并问自己如何编写程序(工具)来为您完成。可以吗?实际上会实现多少自动化,还有多少手动?

  5. 祝福。

答案 5 :(得分:4)

这可以用于简单的算法,但是O(n ^ 2 lg n)或O(n lg ^ 2 n)呢?

你可以很容易地在视觉上被愚弄。

如果它是一个非常糟糕的算法,也许即使在n = 10时它也不会返回。

答案 6 :(得分:3)

证明这是不可判定的:

假设我们有一些算法HALTS_IN_FN(程序,函数),它确定程序是否在所有n的O(f(n))中停止,对于某些函数f。

设P为以下程序:

if(HALTS_IN_FN(P,f(n)))
{
    while(1);
}
halt;

由于功能和程序是固定的,因此该输入上的HALTS_IN_FN是恒定时间。如果HALTS_IN_FN返回true,程序将永远运行,当然不会在任何f(n)的O(f(n))中停止。如果HALTS_IN_FN返回false,则程序在O(1)时间内停止。

因此,我们有一个悖论,一个矛盾,因此该程序是不可判定的。

答案 7 :(得分:2)

我认为自动完成此操作几乎是不可能的。请记住,O(g(n))是最坏情况的上限,许多函数的性能优于许多数据集。您必须为每个数据集找到最坏情况的数据集才能进行比较。对于许多算法来说,这本身就是一项艰巨的任务。

答案 8 :(得分:2)

Jeffrey L Whitledge是对的。从暂停问题中简单地减少证明这是不可判定的......

另外,如果我能编写这个程序,我会用它来解决P vs NP,并且有100万美元...... B - )

答案 9 :(得分:2)

很多人评论说这在理论上是一个固有的无法解决的问题。很公平,但除此之外,即使解决任何问题,除了最微不足道的情况之外,似乎都非常困难。

假设您的程序具有一组嵌套循环,每个循环都基于数组中的项数。为O(n ^ 2)。但是如果内循环仅在非常特定的情况下运行呢?平均来说,它是在aprox log(n)情况下运行的。突然间,我们的“明显”O(n ^ 2)算法实际上是O(n log n)。编写一个可以确定内循环是否会运行的程序,以及频率,可能比原始问题更难。

记住O(N)不是上帝;高常数可以并且将改变比赛场地。 Quicksort算法当然是O(n log n),但是当递归变得足够小时,比如说大约20项左右,快速排序的许多实现会将策略改为单独的算法,因为它实际上更快地进行不同类型的排序比如插入排序的O(N)更差,但常数要小得多。

因此,了解您的数据,进行有根据的猜测并进行测试。

答案 10 :(得分:1)

运行此类基准时,您还必须小心。某些算法的行为严重依赖于输入类型。

以Quicksort为例。这是最坏情况的O(n²),但通常是O(nlogn)。对于两个相同大小的输入。

旅行推销员(我认为,不确定)O(n²)(编辑:蛮力算法的正确值为0(n!)),但大多数算法都相当不错近似解决方案更快。

这意味着基准结构必须在大多数情况下临时调整。想象一下,为所提到的两个例子写一些通用的东西。这将是非常复杂的,可能无法使用,并且可能会给出不正确的结果。

答案 11 :(得分:1)

好吧,既然你无法证明一个函数是否会停止,我想你会问一点。

否则@Godeke有它。

答案 12 :(得分:0)

我正在使用一个big_O库(link here),该库适合执行时间对自变量n的更改,以推断增长类O()的顺序。

该软件包通过针对每个类别的增长行为测量收集的数据中的残差来自动建议最佳拟合类别。

检查this answer中的代码。

输出示例

Measuring .columns[::-1] complexity against rapid increase in # rows
--------------------------------------------------------------------------------
Big O() fits: Cubic: time = -0.017 + 0.00067*n^3
--------------------------------------------------------------------------------
Constant: time = 0.032                                        (res: 0.021)
Linear: time = -0.051 + 0.024*n                               (res: 0.011)
Quadratic: time = -0.026 + 0.0038*n^2                         (res: 0.0077)
Cubic: time = -0.017 + 0.00067*n^3                            (res: 0.0052)
Polynomial: time = -6.3 * x^1.5                               (res: 6)
Logarithmic: time = -0.026 + 0.053*log(n)                     (res: 0.015)
Linearithmic: time = -0.024 + 0.012*n*log(n)                  (res: 0.0094)
Exponential: time = -7 * 0.66^n                               (res: 3.6)
--------------------------------------------------------------------------------

答案 13 :(得分:0)

正如其他人所说,这在理论上是不可能的。但实际上,你可以对一个函数是O( n )还是O( n ^ 2)做出有根据的猜测,如只要你有时不介意错。

第一次算法,在各种 n 的输入上运行它。在log-log graph上绘制点。通过点绘制最合适的线。如果该线很好地拟合了所有点,那么数据表明该算法是O( n ^ k ),其中 k 是斜率这条线。

我不是统计学家。你应该把这一切都带上一粒盐。但实际上,我已经在性能回归的自动化测试环境中完成了这项工作。 The patch here包含一些JS代码。

答案 14 :(得分:0)

我不知道你这样做的目的是什么,但我在教学的课程中遇到了类似的问题。学生们被要求实施一些复杂的工作。

为了不手动检查他们的解决方案并阅读他们的代码,我们使用了@Godeke建议的方法。目标是找到使用链接列表而不是使用压缩搜索树的学生,或者实现冒泡排序而不是堆排序的学生(即不能以所需复杂性工作的实现 - 但实际上没有读取他们的代码)。

令人惊讶的是,结果并没有透露被骗的学生。这可能是因为我们的学生诚实并且想要学习(或者只是知道我们会检查这个;-))。如果输入很小,或者输入本身是有序的,则可能会错过作弊学生。对于没有作弊但具有较大常数值的学生来说,这也是错误的。

但是尽管存在可能的错误,但这非常值得,因为它节省了大量的检查时间。

答案 15 :(得分:0)

我认为这不可能以全自动方式实现,因为输入的类型和结构在函数之间有很大差异。

答案 16 :(得分:-1)

如果你有很多同质的计算资源,我会把它们对几个样本进行计时并进行线性回归,然后简单地选择最高项。

答案 17 :(得分:-2)

很容易得到一个指示(例如“是函数线性?子线性?多项式?指数”)

很难找到确切的复杂性。

例如,这是一个Python解决方案:您提供函数,以及为其创建大小为N的参数的函数。您将获得要绘制的(n,时间)值列表,或执行regression analysis。为了速度,它只需要一次,以获得一个非常好的指示,它必须多次计时以最大限度地减少环境因素的干扰(例如使用timeit模块)。

import time
def measure_run_time(func, args):
  start = time.time()
  func(*args)
  return time.time() - start

def plot_times(func, generate_args, plot_sequence):
  return [
    (n, measure_run_time(func, generate_args(n+1)))
    for n in plot_sequence
  ]

并使用它来计算冒泡时间:

def bubble_sort(l):
  for i in xrange(len(l)-1):
    for j in xrange(len(l)-1-i):
      if l[i+1] < l[i]:
        l[i],l[i+1] = l[i+1],l[i]

import random
def gen_args_for_sort(list_length):
  result = range(list_length) # list of 0..N-1
  random.shuffle(result) # randomize order
  # should return a tuple of arguments
  return (result,)

# timing for N = 1000, 2000, ..., 5000
times = plot_times(bubble_sort, gen_args_for_sort, xrange(1000,6000,1000))

import pprint
pprint.pprint(times)

这印在我的机器上:

[(1000, 0.078000068664550781),
 (2000, 0.34400010108947754),
 (3000, 0.7649998664855957),
 (4000, 1.3440001010894775),
 (5000, 2.1410000324249268)]