经验估计大O时间效率

41

背景

我想通过基准测试来估计库中一些方法的大O复杂度性能。我不需要精确值——只需显示某些东西为O(1), O(logn), O(n), O(nlogn), O(n^2)或更糟。由于大O表示上限,因此对于某些东西的O(log logn)进行O(logn)的估计并不是问题。

目前,我的想法是为每个大O找到最适合数据的常数乘数k(但会优于所有结果),然后选择拟合效果最好的大O。

问题

  1. 有比我所想的更好的方法吗?如果有,是什么?
  2. 否则,有人可以指点我如何估算最佳拟合的k值,并比较每条曲线与数据的拟合程度吗?

注释和限制

鉴于迄今为止的评论,我需要澄清一些事情:

  • 这需要自动化。我无法“查看”数据并进行判断。
  • 我将使用多个n大小来对方法进行基准测试。对于每个大小n,我将使用经过验证的基准测试框架,以提供可靠的统计结果。
  • 我实际上已经知道将要测试的大部分方法的大O复杂度。我的主要意图是为它们提供性能回归测试。
  • 代码将使用Scala编写,并且可以使用任何免费的Java库。

示例

以下是我想测量的一种类型的示例。我有一个具有此签名的方法:

def apply(n: Int): A
给定一个 n,它将返回序列的第n个元素。根据现有实现,这种方法可以具有O(1),O(logn)或O(n),而小的更改可能会错误地使其使用次优实现。或者更容易地说,可能会使某些依赖于它的其他方法使用其次优版本。

2
你能否通过比较不同输入大小的运行来避免查找k?例如,在大小为100、1000、10000、100000等的排序列表上多次运行二分查找,最终应该会显示出你要寻找的模式... - Rob I
也许你可以为一个元素集合获取一个因子k,并计算k*O(f(n))的不同估计值,找到每种方法的即时上界?这样你就有了最坏情况的f(n)? - fge
1
@JacoVanNiekerk:一般情况下,这是不可能的。(至少,如果库是用图灵完备语言编写的,并且问题的“java”和“scala”标签表明它是!) - ruakh
2
问题在于对于“小”输入,一切都是O(1)。真正的问题是什么是“小”,即从何时开始您的算法开始受益,例如O(n log n)。有些算法在输入大小为100时开始显示其性能,而其他算法需要10^1000:log(log(10^1000))小于10。 - Igor Korkhov
由于从理论上讲你所要求的是不可能的,我建议你考虑一种方法,它可以产生一些“斜率”(如指数)和置信度值;就像分布的均值和方差一样。 - ziggystar
显示剩余6条评论
10个回答

17
为了开始,您需要做一些假设。
1. n相对于任何常数项都很大。 2. 您可以有效地随机输入数据。 3. 您可以采样足够密集的数据以更好地了解运行时间的分布情况。
特别地,(3)很难与(1)共同实现。因此,您可能会得到一个最坏情况下呈指数增长的算法,但从未遇到过该最坏情况,因此认为您的算法平均而言比它实际上好得多。
说了这么多,您只需要任何标准曲线拟合库。Apache Commons Math有一个完全足够的库。然后,您可以创建一个包含您想要测试的所有常见术语的函数(例如:常数、log n、n、n log n、nn、nn*n、e^n),或者取对数并拟合指数,然后如果您得到的指数不接近整数,请看看是否加入log n可以获得更好的拟合结果。
更详细地说,如果你适合使用 C*x^a 表示 Ca,或者更容易的是 log C + a log x,你就可以得到指数 a;在一次性处理所有常见项的方案中,你将获得每个项的权重,因此如果你有 n*n + C*n*log(n),其中 C 很大,你也会捕捉到该项。

你需要足够变化大小,以便你可以区分不同的情况(如果你关心对数项可能会很难),并且安全地比你有的参数更多的不同大小(可能 3 倍的过剩会开始变得可行,只要你总共运行至少十几次)。


编辑:这里有一份Scala代码,可以为您完成所有操作。我不会解释每个小部分,而是让你自己去探索它; 它使用C * x ^ a拟合实现了上面的方案,并返回((a,C),(a的下限,a的上限))。由于您可以从运行几次中看到,这些边界非常保守。 C 的单位为秒(a 无单位),但不要太相信它,因为有一些循环开销(还有一些噪音)。

class TimeLord[A: ClassManifest,B: ClassManifest](setup: Int => A, static: Boolean = true)(run: A => B) {
  @annotation.tailrec final def exceed(time: Double, size: Int, step: Int => Int = _*2, first: Int = 1): (Int,Double) = {
    var i = 0
    val elapsed = 1e-9 * {
      if (static) {
        val a = setup(size)
        var b: B = null.asInstanceOf[B]
        val t0 = System.nanoTime
        var i = 0
        while (i < first) {
          b = run(a)
          i += 1
        }
        System.nanoTime - t0
      }
      else {
        val starts = if (static) { val a = setup(size); Array.fill(first)(a) } else Array.fill(first)(setup(size))
        val answers = new Array[B](first)
        val t0 = System.nanoTime
        var i = 0
        while (i < first) {
          answers(i) = run(starts(i))
          i += 1
        }
        System.nanoTime - t0
      }
    }
    if (time > elapsed) {
      val second = step(first)
      if (second <= first) throw new IllegalArgumentException("Iteration size increase failed: %d to %d".format(first,second))
      else exceed(time, size, step, second)
    }
    else (first, elapsed)
  }

  def multibench(smallest: Int, largest: Int, time: Double, n: Int, m: Int = 1) = {
    if (m < 1 || n < 1 || largest < smallest || (n>1 && largest==smallest)) throw new IllegalArgumentException("Poor choice of sizes")
    val frac = (largest.toDouble)/smallest
    (0 until n).map(x => (smallest*math.pow(frac,x/((n-1).toDouble))).toInt).map{ i => 
      val (k,dt) = exceed(time,i)
      if (m==1) i -> Array(dt/k) else {
        i -> ( (dt/k) +: (1 until m).map(_ => exceed(time,i,first=k)).map{ case (j,dt2) => dt2/j }.toArray )
      }
    }.foldLeft(Vector[(Int,Array[Double])]()){ (acc,x) =>
      if (acc.length==0 || acc.last._1 != x._1) acc :+ x
      else acc.dropRight(1) :+ (x._1, acc.last._2 ++ x._2)
    }
  }

  def alpha(data: Seq[(Int,Array[Double])]) = {
    // Use Theil-Sen estimator for calculation of straight-line fit for exponent
    // Assume timing relationship is t(n) = A*n^alpha
    val dat = data.map{ case (i,ad) => math.log(i) -> ad.map(x => math.log(i) -> math.log(x)) }
    val slopes = (for {
      i <- dat.indices
      j <- ((i+1) until dat.length)
      (pi,px) <- dat(i)._2
      (qi,qx) <- dat(j)._2
    } yield (qx - px)/(qi - pi)).sorted
    val mbest = slopes(slopes.length/2)
    val mp05 = slopes(slopes.length/20)
    val mp95 = slopes(slopes.length-(1+slopes.length/20))
    val intercepts = dat.flatMap{ case (i,a) => a.map{ case (li,lx) => lx - li*mbest } }.sorted
    val bbest = intercepts(intercepts.length/2)
    ((mbest,math.exp(bbest)),(mp05,mp95))
  }
}

请注意,multibench方法预计需要大约sqrt(2)nm*time的时间运行,假设使用静态初始化数据,并且与您正在运行的内容相比相对便宜。以下是一些参数选取为大约15秒运行时间的示例:
val tl1 = new TimeLord(x => List.range(0,x))(_.sum)  // Should be linear
// Try list sizes 100 to 10000, with each run taking at least 0.1s;
// use 10 different sizes and 10 repeats of each size
scala> tl1.alpha( tl1.multibench(100,10000,0.1,10,10) )
res0: ((Double, Double), (Double, Double)) = ((1.0075537890632216,7.061397125245351E-9),(0.8763463348353099,1.102663784225697))

val longList = List.range(0,100000)
val tl2 = new TimeLord(x=>x)(longList.apply)    // Again, should be linear
scala> tl2.alpha( tl2.multibench(100,10000,0.1,10,10) )
res1: ((Double, Double), (Double, Double)) = ((1.4534378213477026,1.1325696181862922E-10),(0.969955396265306,1.8294175293676322))

// 1.45?!  That's not linear.  Maybe the short ones are cached?
scala> tl2.alpha( tl2.multibench(9000,90000,0.1,100,1) )
res2: ((Double, Double), (Double, Double)) = ((0.9973235607566956,1.9214696731124573E-9),(0.9486294398193154,1.0365312207345019))

// Let's try some sorting
val tl3 = new TimeLord(x=>Vector.fill(x)(util.Random.nextInt))(_.sorted)
scala> tl3.alpha( tl3.multibench(100,10000,0.1,10,10) )
res3: ((Double, Double), (Double, Double)) = ((1.1713142886974603,3.882658025586512E-8),(1.0521099621639414,1.3392622111121666))
// Note the log(n) term comes out as a fractional power
// (which will decrease as the sizes increase)

// Maybe sort some arrays?
// This may take longer to run because we have to recreate the (mutable) array each time
val tl4 = new TimeLord(x=>Array.fill(x)(util.Random.nextInt), false)(java.util.Arrays.sort)
scala> tl4.alpha( tl4.multibench(100,10000,0.1,10,10) )
res4: ((Double, Double), (Double, Double)) = ((1.1216172965292541,2.2206198821180513E-8),(1.0929414090177318,1.1543697719880128))

// Let's time something slow
def kube(n: Int) = (for (i <- 1 to n; j <- 1 to n; k <- 1 to n) yield 1).sum
val tl5 = new TimeLord(x=>x)(kube)
scala> tl5.alpha( tl5.multibench(10,100,0.1,10,10) )
res5: ((Double, Double), (Double, Double)) = ((2.8456382116915484,1.0433534274508799E-7),(2.6416659356198617,2.999094292838751))
// Okay, we're a little short of 3; there's constant overhead on the small sizes

无论如何,对于所述用例——检查订单是否更改——这可能是足够的,因为您可以在设置测试时稍微调整一下值,以确保它们给出合理的结果。也可以创建启发式算法来搜索稳定性,但这可能有点过度。
(顺便说一句,在这里没有明确的预热步骤;Theil-Sen估计器的强健拟合应该使其不必要适用于合理大的基准测试。这也是我不使用任何其他测试框架的原因;它所做的任何统计都会从这个测试中失去效力。)

再次编辑:如果你用以下方法替换alpha方法:

  // We'll need this math
  @inline private[this] def sq(x: Double) = x*x
  final private[this] val inv_log_of_2 = 1/math.log(2)
  @inline private[this] def log2(x: Double) = math.log(x)*inv_log_of_2
  import math.{log,exp,pow}

  // All the info you need to calculate a y value, e.g. y = x*m+b
  case class Yp(x: Double, m: Double, b: Double) {}

  // Estimators for data order
  //   fx = transformation to apply to x-data before linear fitting
  //   fy = transformation to apply to y-data before linear fitting
  //   model = given x, slope, and intercept, calculate predicted y
  case class Estimator(fx: Double => Double, invfx: Double=> Double, fy: (Double,Double) => Double, model: Yp => Double) {}
  // C*n^alpha
  val alpha = Estimator(log, exp, (x,y) => log(y), p => p.b*pow(p.x,p.m))
  // C*log(n)*n^alpha
  val logalpha = Estimator(log, exp, (x,y) =>log(y/log2(x)), p => p.b*log2(p.x)*pow(p.x,p.m))

  // Use Theil-Sen estimator for calculation of straight-line fit
  case class Fit(slope: Double, const: Double, bounds: (Double,Double), fracrms: Double) {}
  def theilsen(data: Seq[(Int,Array[Double])], est: Estimator = alpha) = {
    // Use Theil-Sen estimator for calculation of straight-line fit for exponent
    // Assume timing relationship is t(n) = A*n^alpha
    val dat = data.map{ case (i,ad) => ad.map(x => est.fx(i) -> est.fy(i,x)) }
    val slopes = (for {
      i <- dat.indices
      j <- ((i+1) until dat.length)
      (pi,px) <- dat(i)
      (qi,qx) <- dat(j)
    } yield (qx - px)/(qi - pi)).sorted
    val mbest = slopes(slopes.length/2)
    val mp05 = slopes(slopes.length/20)
    val mp95 = slopes(slopes.length-(1+slopes.length/20))
    val intercepts = dat.flatMap{ _.map{ case (li,lx) => lx - li*mbest } }.sorted
    val bbest = est.invfx(intercepts(intercepts.length/2))
    val fracrms = math.sqrt(data.map{ case (x,ys) => ys.map(y => sq(1 - y/est.model(Yp(x,mbest,bbest)))).sum }.sum / data.map(_._2.length).sum)
    Fit(mbest, bbest, (mp05,mp95), fracrms)
  }

然后,当有对数项时,您可以得到指数的估计值——误差估计存在于选择是使用对数项还是不使用对数项的正确方式之间,但由您决定(即我假设您最初将监督此过程并阅读输出的数字):

val tl3 = new TimeLord(x=>Vector.fill(x)(util.Random.nextInt))(_.sorted)
val timings = tl3.multibench(100,10000,0.1,10,10)

// Regular n^alpha fit
scala> tl3.theilsen( timings )
res20: tl3.Fit = Fit(1.1811648421030059,3.353753446942075E-8,(1.1100382697696545,1.3204652930525234),0.05927994882343982)

// log(n)*n^alpha fit--note first value is closer to an integer
//   and last value (error) is smaller
scala> tl3.theilsen( timings, tl3.logalpha )
res21: tl3.Fit = Fit(1.0369167329732445,9.211366397621766E-9,(0.9722967182484441,1.129869067913768),0.04026308919615681)

(编辑:修正了RMS计算,使其实际上是平均值,另外还演示了您只需要进行一次定时,然后可以尝试两种拟合。)

1
这很酷,虽然我有点担心它在长期使用中的有效性。我会试一试。 - Daniel C. Sobral
1
@DanielC.Sobral - 实验上区分对数项和小分数幂有点棘手,特别是当像缓存局部性这样的因素对较小的尺寸产生很大影响时。我想我可以添加一个对数估计器,并报告每个估计值的中位误差。 - Rex Kerr
1
@DanielC.Sobral - 现在你可以指定它应该假定一个长期的日志(请参见新代码和新示例)。 - Rex Kerr

14

我认为你的方法通常不会奏效。

问题在于,“大O”复杂度是基于某个缩放变量趋近无穷大时的极限,对于该变量较小的值,性能行为可能看似适合完全不同的曲线。

问题在于,使用实证方法你永远无法知道缩放变量是否足够大以使极限在结果中显现。

另一个问题在于,如果你在Java / Scala中实现这个方法,你必须花费相当大的功夫来消除与诸如JVM预热(例如类加载,JIT编译,堆调整)和垃圾回收等因素相关的时间测量的扭曲和“噪音”。

最后,没有人会对复杂度的实证估计放心。或者说,如果他们理解复杂度分析的数学知识,他们就不会这样做。


后续

回应这条评论:

你使用的样本数量越多、越大,估算的意义就会有很大提高。

这是正确的,不过我的观点是,你(Daniel)没有考虑到这一点。

此外,运行时函数通常具有可利用的特殊特点;例如,算法往往在某些巨大的n处不会改变其行为。

对于简单情况是这样的。

对于复杂的情况和现实情况来说,这是一个值得怀疑的假设。例如:

  • 假设某个算法使用一个具有固定大小但较大的主散列数组的哈希表,并使用外部列表处理冲突。对于小于主散列数组大小的N(==条目数),大多数操作的行为将看起来符合O(1)。只有当N远大于此时,才能通过拟合曲线来检测到真正的O(N)行为。

  • 假设算法使用了大量的内存或网络带宽。通常情况下,它会工作得很好,直到达到资源限制,然后性能会急剧下降。如何考虑这个问题?如果它是“经验复杂度”的一部分,您如何确保到达过渡点?如果您想要排除它,该怎么办?


  • 你的估计结果的重要性将随着使用更多和更大的样本而显著提高。此外,运行时函数通常具有特殊的特征,可以被利用;例如,算法倾向于在某些巨大的 n 改变它们的行为。 - Raphael
    我从未见过有人像你提到的那样发布关于具有固定大小数组的哈希表或使用大量内存/网络带宽的算法的大O时间效率的警告。我看到的值似乎总是假定有无限的带宽/内存。当然,这并不现实,但也许这超出了此符号的范围。 - Gili
    @Gili - 这个问题是关于经验估算算法实现的。 (如果没有实现,就无法运行算法,如果无法运行,则无法测量它)。 如果我们谈论运行真实算法的实现,则(通常)必须考虑内存使用情况,因为它会影响时间测量。 - Stephen C

    7
    如果您希望通过经验估计时间复杂度,可以测量执行指数级增长的操作所需的时间。使用比率可以确定您估计的函数。

    例如,如果1000个操作与10000个操作(10倍)的比率是(先测试较长的那个),则需要执行实际数量的操作以查看您所拥有的范围的顺序。

    • 1x => O(1)
    • 1.2x => O(ln ln n)
    • 〜2-5x => O(ln n)
    • 10x => O(n)
    • 20-50x => O(n ln n)
    • 100x => O(n ^ 2)

    这只是一个估计值,因为时间复杂度是针对理想机器而设计的,某些内容应该能够在数学上证明而不是进行测量。

    例如,许多人试图通过经验来证明PI是一个分数。当他们测量自己制作的圆的周长和直径之比时,它总是一个分数。最终,人们普遍认为PI不是一个分数。

    这绝对不足以确定运行时复杂度。例如,考虑一个执行时间为Y,输入大小为X的函数。令Y(X) = aX^2 + bX + C,并让系数为a=1、b=(10-11*10^7)/10^4、c=10^7。由于存在X^2项,因此它显然具有O(X^2)复杂度。然而,如果我们调查你提到的执行时间,我们可以看到:Y(1000)=1Y(10000)=10。因此,你的分类策略会错误地将其归类为O(X)。 - Alderath
    当然,该函数的构建是为了满足Y(1000) = 1Y(10000) = 10,其系数可能对于任何有用的真实算法都不太自然(但构建大约具有该执行时间函数的愚蠢算法是肯定可能的)。然而,主要观点是不能假设例如O(N^2)算法在描述执行时间的函数中只有O(N^2)项。事实上,可能存在其他项会使这种估计策略不准确。 - Alderath
    你需要执行适量的操作来进行测试。如果你永远不会执行超过10,000次操作,并且它在该范围内的规模为O(n),那么知道只有在尝试更多操作时才能得到o(n^2)如何帮助你? - Peter Lawrey
    Pi是两个量的比值,这使它成为一个数字和一个分数。它是一个无理数,但可以用连分数/递归分数来表示。 - Indolering

    5
    我们最近实现了一个工具,用于对JVM代码进行半自动平均运行时间分析。您甚至不需要访问源代码。它尚未发布(仍在解决一些可用性缺陷),但希望很快会发布。
    它基于程序执行的最大似然模型[1]。简而言之,字节码被增加了成本计数器。然后在一堆输入上运行目标算法(如果您想要分布式运行)。使用涉及启发式方法(最小二乘法等)将聚合计数器外推到函数。从这些数据中,更多科学研究可以得出平均运行时间渐近估计值(例如3.576n-1.23log(n)+1.7)。例如,该方法能够以高精度复制Knuth和Sedgewick所做的严格经典分析。
    与其他人发布的内容相比,这种方法的最大优点是您独立于时间估计,特别是独立于机器、虚拟机甚至编程语言。您真正地了解算法的信息,没有任何噪音。
    而且,也许是致命功能,它配有完整的GUI,引导您完成整个过程。

    请参见我的cs.SE答案,了解更多细节和进一步的参考资料。 您可以在此处找到初步网站(包括工具的测试版和已发布的论文)。

    (请注意,平均运行时间可以通过这种方式估计,而最坏情况下的运行时间除非您知道最坏情况,否则永远无法估计。如果您知道最坏情况,则可以将平均情况用于最坏情况分析;只需将工具仅提供最坏情况实例即可。总的来说,运行时间的界限无法确定。)


    1. 最大似然算法和数据结构分析,作者为U. Laube和M.E. Nebel(2010年)。[预印本]

    嗯,我们不都感兴趣吗? - Indolering
    @Indolering 看来不是这样的,因为请求花了九个月才出现。 :) 到现在为止,我们有一个网站和一篇论文即将发表;请参见编辑。 论文会随着它们的出现而添加到列表中。 - Raphael
    这篇论文的链接已经失效,但我猜想它指的是论文《算法和数据结构的最大似然分析》(作者Ulrich Laube 和 Markus E. Nebel,发表在《理论计算机科学》杂志上,卷号为411,期号为1, 2010年,188-212页),可以在这里找到:http://wwwagak.cs.uni-kl.de/component/remository/func-startdown/15/?Itemid=277 - Steohan
    @Steohan 感谢您通知我链接失效的问题。我已将文章的引用更改为更稳健的一个。不幸的是,我没有这个工具的替代链接。也许它会回来的。 - Raphael

    4
    你想要达到的目标通常是不可能的。甚至在一般情况下,算法停止的事实也无法得到证明(参见 Halting Problem)。即使它在你的数据上停止,你仍然不能通过运行它来推断复杂性。例如,冒泡排序的复杂度为O(n^2),而在已排序的数据上,它的表现就像是O(n)。没有办法选择“适当”的数据来估计未知算法的最坏情况。

    8
    这种方法有以下几个问题:1. 我不是在证明,而是在估计——停机问题涉及证明,而不是统计学。事实上,虽然在一般情况下无法证明完成,但对于我将要测试的方法来说,这是微不足道的;2. 我不会测试未知算法,我可以并且会为它们选择适当的数据;3. 它实际上没有回答我的任何问题。 - Daniel C. Sobral
    2
    但是,如果您正在测试已知的算法,那么为什么需要运行它们以获取它们的复杂度呢?对于所有实际使用的算法,它们的复杂度已经是已知的。 - Igor Korkhov
    在您的情况下,“性能回归”的可能原因是什么?您是否在谈论例如大型数据集上过度磁盘交换等问题? - Igor Korkhov
    1
    @IgorKorkhov - 可能是像在之前的n log n算法中意外引入了一个n^2项这样的事情。 - Rex Kerr
    1
    @Daniel为什么不让你的方法除了结果外再返回一个标识符呢? - ziggystar
    显示剩余2条评论

    1

    您应该考虑更改任务的关键方面。

    将您使用的术语更改为:“估计算法的运行时间”或“设置性能回归测试”

    您能否估计算法的运行时间?您建议尝试不同的输入大小,并测量某些关键操作或所需时间。然后,对于一系列输入大小,您计划以编程方式估计算法的运行时间是否没有增长、恒定增长、指数增长等。

    因此,您有两个问题,一个是运行测试,另一个是随着输入集的增长以编程方式估计增长率。这听起来是一个合理的任务。


    1

    我不确定我完全理解你想要什么。但我明白你测试自己的代码,所以你可以修改它,例如注入观察语句。否则,你可以使用某种形式的方面编织?

    你可以向数据结构添加可重置计数器,然后每次调用特定子函数时增加它们的数量。你可以使这些计数@elidable,这样它们将在部署库中消失。

    然后对于给定的方法,比如delete(x),你将使用各种自动生成的数据集进行测试,尝试给它们一些偏斜等,并收集计数。正如Igor指出的那样,你无法验证数据结构不会违反大O边界,但你至少能够断言,在实际实验中,给定的限制计数从未超过(例如,在树中下降一个节点永远不会超过4 * log(n)次)--因此你可以检测到一些错误。

    当然,你需要一些假设,例如,在你的计算机模型中调用方法是O(1)。


    你的推理是有缺陷的,因为有限的测量无法得出上界的结论。然而,你的人类猜测通常是正确的,只是在数字上很难证明。最小二乘法是一种可以尝试的方法,与对立数据一起使用;但是,你必须准备尝试许多函数,因为系数和主导项会严重影响最小二乘分数。 - Raphael
    我的推理并不错误。我是在说您不能正面证明结构遵循上限。但是,您可以通过负面观察算法涉及步骤的假定计数的违规情况来消除错误。 - 0__
    我可以将任何有限数据集绑定为c*log(n),其中c是某个常数。在特定情况下,如何表达这是一个比d*n更差的界限? - Raphael
    @Raphael:比这更糟糕的是:我可以将任何有限数据集绑定到某个(可能非常大的)常数C,并错误地得出结论,即任何算法都是O(1)或永远不会停止。话虽如此,我认为OP问题的实际应用方面非常有趣。 - Igor Korkhov
    也许我漏掉了什么。但是假设我实现了一个算法,根据论文中的证明,它的复杂度不超过 m * log n 步。然后我可以观察我的实现,并通过断言最多执行了 m log n 步来排除错误。这是一种消除错误的程序,本质上并不能保证没有剩余的错误。但我认为这不是问题所在。请注意,c 不是“某个常数”,而是一个定义好的常数。 - 0__
    我明白了。我猜你可以这样做,是的——如果你严格按照已证明的算法去做。但我认为在大多数情况下你不会这样做。 - Raphael

    1
    我事实上已经知道将被测试的大多数方法的大O。我的主要意图是为它们提供性能回归测试。
    这个需求很关键。你想用最少的数据检测异常值(因为测试应该快速),而且根据我的经验,将曲线拟合到复杂递归的数字评估、线性回归等会过度拟合。我认为你的初始想法是好的。
    我会怎样实现它呢?首先准备一个预期复杂度函数g1、g2等列表,对于数据f,测试每个i中f/gi + gi/f与常数有多接近。使用最小二乘代价函数,只需计算该数量对于每个i的方差并报告最小值。在最后检查方差并手动检查异常拟合。

    0
    为了对程序复杂度进行实证分析,您需要运行(和计时)给定算法的10、50、100、500、1000等输入元素。然后您可以绘制结果并确定最常见基本类型的最佳拟合函数顺序:常数、对数、线性、nlogn、二次、立方、高阶多项式、指数。这是负载测试的正常部分,通过该测试确保算法首先按照理论运行,并且其实际性能符合预期,尽管它的理论复杂度比较高(一个每步需要5分钟的对数时间复杂度算法将会输给一个每步只需要几毫秒的二次复杂度算法,除了最高基数测试)。
    编辑:将其分解,该算法非常简单:
    定义一个列表N,其中包含您想要评估性能的各种基数(例如10、100、1000、10000等)
    对于N中的每个元素X:
    创建一个适当的测试数据集,其中包含X个元素。
    启动秒表或确定并存储当前系统时间。
    在X元素测试集上运行算法。
    停止秒表或再次确定系统时间。

    起始时间和结束时间之间的差异就是算法在X个元素上运行的时间。

    对于每个N中的X重复。

    绘制结果;给定X元素(x轴),算法需要T时间(y轴)。随着X的增加,T增加的最接近基本函数是你的大O近似。正如Raphael所说,这个近似仅仅是一个近似,不能得到非常精细的区别,例如N的系数,这可能使一个N²算法和一个2N²算法之间的差异(两者技术上都是O(N²) ,但是在相同数量的元素下,其中一个将执行两倍快)。


    好的,没错,这就是我说过的计划。那么,关于我需要用到的具体算法的信息在哪里呢? :-) - Daniel C. Sobral
    1
    是的,因为我可以提供10个情节,你无法可靠地评估其复杂性。例如,尝试将n^2n^(7/3)分开。我们遇到了一些奇怪的东西(当然,这是在精确数据集上,而不是嘈杂的时间测量中)。 - Raphael
    大O符号始终是一种估计方法。我甚至不会尝试区分N^2和N^(7/3);从经验上来看,两者都比N^3更接近N^2,因此如果您想要一个经验估计值,则会得到N^2。像N^2.3333这样的微小区别将通过执行路径分析进行理论推导。 - KeithS

    0

    我也想分享我的实验。从理论上来说没有什么新的东西,但它是一个完全功能的Python模块,可以轻松扩展。

    主要内容:

    • 它基于scipy Python库中的curve_fit函数,允许将任何函数拟合到给定点集中,最小化平方差之和;

    • 由于测试是以指数级别增加问题规模的,因此靠近起点的点会有更大的权重,这并不有助于确定正确的逼近值,因此我认为简单的线性插值可以重新分配点,使其均匀分布;

    • 我们正在尝试拟合的逼近值集合完全在我们的控制范围内;我添加了以下内容:

            def fn_linear(x, k, c):
                return k * x + c
    
            def fn_squared(x, k, c):
                return k * x ** 2 + c
    
            def fn_pow3(x, k, c):
                return k * x ** 3 + c
    
            def fn_log(x, k, c):
                return k * np.log10(x) + c
    
            def fn_nlogn(x, k, c):
                return k * x * np.log10(x) + c
    

    这是一个完全功能的 Python 模块,可以进行操作:https://gist.github.com/gubenkoved/d9876ccf3ceb935e81f45c8208931fa4,它会生成一些图片(请注意--每个示例有 4 个图表,具有不同的轴比例)。

    sorting

    bisect

    cartesian product


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接