算法分析的魔力背后是否有一个系统?


159

关于如何分析算法的运行时间存在很多问题(例如,参见)。许多都是类似的,例如那些要求对嵌套循环或分而治之算法进行成本分析的方法,但是大多数答案似乎都是量身定制的。

另一方面,另一个通用问题的答案通过一些示例解释了更大的图景(尤其是关于渐近分析),但没有说明如何弄脏您的手。

有没有一种结构化的,通用的方法来分析算法的成本?成本可能是运行时间(时间复杂度),也可能是某种其他成本度量,例如执行的比较次数,空间复杂度或其他。

这应该成为一个参考问题,可以用来指导初学者。因此其范围比平常大。请小心给出一般的,有说服力的答案,至少由一个示例说明了这一点,但仍然涵盖了许多情况。谢谢!


3
感谢StackEdit的作者使撰写如此长的文章变得很方便,而我的Beta版读者FrankWJuhoGillesSebastian则帮助我消除了早期草稿中的许多缺陷。
拉斐尔

1
嘿@Raphael,这真是棒极了。我以为我建议将其合并为PDF以便分发?这种事情可能会成为真正有用的参考。
2014年

1
@hadsed:谢谢,我很高兴对您有用!现在,我更喜欢散布到该帖子的链接。但是,SE用户内容“在cc by-sa 3.0下需要具有署名的许可”(请参阅​​页脚),因此只要提供了署名,任何人都可以从中创建PDF。
拉斐尔

2
我对此并不特别称职,但是在任何情况下都没有提到Master定理是正常的吗?
2014年

1
@babou我不知道“正常”在这里是什么意思。从我的角度来看,Master定理在这里无济于事:这是关于分析算法的问题,Master定理是解决(某些)递归问题的非常具体的工具。由于数学已经在其他地方进行了介绍(例如,此处),因此我选择此处仅涵盖从算法到数学的部分。我在回答中引用了涉及数学工作的帖子。
拉斐尔

Answers:


134

将代码翻译成数学

给定(或多或少)形式化的操作语义,您可以将算法的(伪)代码从字面上转换为可以为您提供结果的数学表达式,前提是您可以将表达式操纵为有用的形式。这对于附加成本度量(例如比较数,交换,语句,内存访问,某些抽象机需求的循环等等)非常有效。

示例:Bubblesort中的比较

考虑以下对给定数组进行排序的算法A

 bubblesort(A) do                   1
  n = A.length;                     2
  for ( i = 0 to n-2 ) do           3
    for ( j = 0 to n-i-2 ) do       4
      if ( A[j] > A[j+1] ) then     5
        tmp    = A[j];              6
        A[j]   = A[j+1];            7
        A[j+1] = tmp;               8
      end                           9
    end                             10
  end                               11
end                                 12

假设我们要执行通常的排序算法分析,即计算元素比较的次数(第5行)。我们立即注意到,该数量不取决于数组的内容A,而仅取决于其长度。这样我们就可以将(嵌套的)循环完全转换为(嵌套的)和。循环变量变为求和变量,范围继续。我们得到:nfor

Ccmpñ=一世=0ñ-2Ĵ=0ñ-一世-21个==ññ-1个2=ñ2

其中是第5行(我们计算)的每次执行费用。1个

示例:在Bubblesort中交换

我将表示,它由线的子程序,以和由Ç Ĵ用于执行该子程序(一次)的费用。P一世ĴijC一世Ĵ

现在,让我们说,我们要算,那是多么经常执行。这是一个“基本块”,它是一个子程序,始终以原子方式执行并具有一定的成本(此处为1)。压缩这样的块是一种有用的简化,我们经常在不考虑或谈论它的情况下就应用它。P681个

通过与上述类似的翻译,我们得出以下公式:

Cswaps(A)=i=0n2j=0ni2C5,9(A(i,j))

表示前阵列的状态Ĵ 的次迭代 P 5 9A(i,j)(i,j)P5,9

请注意,我使用而不是n作为参数。我们很快就会明白原因。我不加Ĵ作为参数Ç 5 9,因为成本不依赖于他们在这里(在统一的成本模型,这是); 一般来说,他们只是可能。AnijC5,9

显然,成本取决于内容(值和,特别是),所以我们必须考虑到这一点。现在我们面临一个挑战:我们如何“解包” Ç 5 9?好吧,我们可以使对A内容的依赖关系明确:P59一种A[j]A[j+1]C59一种

C59一种一世Ĵ=C5一种一世Ĵ+{1个一种一世Ĵ[Ĵ]>一种一世Ĵ[Ĵ+1个]0其他

对于任何给定的输入数组,这些成本是明确定义的,但是我们需要更笼统的说明;我们需要做出更强有力的假设。让我们研究三种典型的情况。

  1. 最坏的情况

    刚刚从看总和并注意到,我们可以找到一个微不足道的上限费用:C59一种一世Ĵ{01个}

    C掉期一种一世=0ñ-2Ĵ=0ñ-一世-21个=ññ-1个2=ñ2

    但是会发生这种情况吗,即达到该上限的?事实证明,是的:如果我们输入成对的不同元素的反向排序数组,则每次迭代都必须执行一次交换¹。因此,我们得出了Bubblesort交换的最坏情况的确切数字。一种

  2. 最好的情况

    相反,有一个小的下限:

    C掉期一种一世=0ñ-2Ĵ=0ñ-一世-20=0

    这也可能发生:在已排序的数组上,Bubblesort不会执行单个交换。

  3. 平均情况

    最坏的情况和最好的情况还存在很大差距。但是掉期的典型数量是多少?为了回答这个问题,我们需要定义“典型”的含义。从理论上讲,我们没有理由偏爱一个输入而不是另一个输入,因此我们通常假定所有可能输入的分布均匀,即每个输入均具有相同的可能性。我们将自己限制为具有成对的不同元素的数组,因此采用随机排列模型。

    然后,我们可以像这样重写我们的费用²:

    Ë[C掉期]=1个ñ一种一世=0ñ-2Ĵ=0ñ-一世-2C59一种一世Ĵ

    现在我们必须超越简单的求和运算。通过查看算法,我们注意到,每一个交换去除只有一个倒置(我们永远只能换neighbours³)。也就是说,在执行交换次数正是反转次数INV 。因此,我们可以替换内部的两个和得到一种一种v一种一种

    Ë[C掉期]=1个ñ一种v一种

    对我们来说幸运的是,平均反演次数已确定为

    Ë[C掉期]=1个2ñ2

    这是我们的最终结果。请注意,这恰好是最坏情况下成本的一半


  1. 请注意,算法是经过精心制定的,因此i = n-1不会执行从未执行任何操作的外部循环的“最后一次”迭代。
  2. ”是“期望值”的数学符号,这里只是平均值。Ë
  3. 我们沿途学习,没有一种仅交换相邻元素的算法在渐近性上可以比Bubblesort渐近地快(甚至是平均水平)-求反的次数是所有此类算法的下限。这适用于例如插入排序选择排序

通用方法

在示例中我们已经看到,我们必须将控制结构转换为数学。我将介绍翻译规则的典型合奏。我们还已经看到,任何给定子程序的成本可能取决于当前状态,即(大致而言)变量的当前值。由于该算法(通常)会修改状态,因此通用方法要注释起来有点麻烦。如果您开始感到困惑,建议您回到该示例或自己编写示例。

我们用表示当前状态(将其想象为一组变量赋值)。当我们执行从状态ψ开始的程序时,我们以状态ψ / P结束(提供终止)。ψPψψ/PP

  • 个人陈述

    仅给出一个语句S;,就将其分配为。这通常是一个常数函数。C小号ψ

  • 表达方式

    如果您具有E形式的表达式E1 ∘ E2(例如,算术表达式,其中可能是加法或乘法),则需要递归累加成本:

    CËψ=C+CË1个ψ+CË2ψ

    注意

    • 操作成本可以不是恒定的,而是依赖于值ë 1ë 2CË1个Ë2
    • 评估表达式可能会改变许多语言的状态,

    因此您可能必须对这个规则保持灵活。

  • 序列

    给定一个程序P作为程序序列Q;R,您将成本添加到

    CPψ=Cψ+C[Rψ/

  • 有条件的

    给定P形式的程序if A then Q else R end,费用取决于状态:

    CPψ=C一种ψ+{Cψ/一种一种 在以下条件下评估为true ψC[Rψ/一种其他

    通常,评估A可能会很好地改变状态,因此会更新各个分支机构的成本。

  • 循环

    给定P形式的程序for x = [x1, ..., xk] do Q end,分配成本

    CPψ=Cinit_for+一世=1个ķCstep_for+Cψ一世{X:=X一世}

    其中是处理前的状态为值与迭代之后,即,被设置为,..., 。ψ一世Qxixx1xi-1

    注意用于循环维护的额外常量;必须创建循环变量()并为其赋值(c step_for)。这很重要,因为Cinit_forCstep_for

    • 计算下一个xi可能会很昂贵,并且
    • for如果执行迭代,则具有空主体的-loop(例如,在使用最佳成本的最佳情况下进行简化后)不会具有零成本。
  • 循环

    给定P形式的程序while A do Q end,分配成本

    CPψ =C一种ψ+{0一种 在以下条件下评估为假 ψCψ/一种+CPψ/一种; 其他

    通过检查算法,这种重复经常可以很好地表示为类似于for循环的总和。

    示例:考虑以下简短算法:

    while x > 0 do    1
      i += 1          2
      x = x/2         3
    end               4
    

    通过应用规则,我们得到

    C1个4{一世:=一世0;X:=X0} =C<+{0X00C+=+C/+C1个4{一世:=一世0+1个;X:=X0/2} 其他

    Cix

    C1个4i

    C1个4X={C>X0C>+C+=+C/+C1个4X/2 其他

    这解决了基本的手段

    C1个4ψ=日志2ψXC>+C+=+C/+C>

    ψ={X:=5}ψX=5

  • 程序调用

    给定某个参数P形式的程序,其中是带有(命名)参数的过程,请分配费用M(x)xMp

    CPψ=C呼叫+C中号ψ球状{p:=X}

    C呼叫ψ

    我掩盖了您可能会对此处的状态产生的一些语义问题。您将要区分全局状态和过程调用的局部状态。假设我们仅在此处传递全局状态,并M获得一个新的局部状态,方法是将的值设置px。此外,x可能是一个表达式,我们(通常)假定在传递它之前先对其求值。

    示例:考虑程序

    fac(n) do                  
      if ( n <= 1 ) do         1
        return 1               2
      else                     3
        return n * fac(n-1)    4
      end                      5
    end                        
    

    根据规则,我们得到:

    C事实{ñ:=ñ0}=C1个5{ñ:=ñ0}=C+{C2{ñ:=ñ0}ñ01个C4{ñ:=ñ0} 其他=C+{C返回ñ01个C返回+C+C呼叫+C事实{ñ:=ñ0-1个} 其他

    请注意,我们无视全局状态,因为fac显然不访问任何状态。这种特定的复发很容易解决

    C事实ψ=ψñC+C返回+ψñ-1个C+C呼叫

我们已经介绍了典型伪代码中将遇到的语言功能。分析高级伪代码时要注意隐藏的成本;如有疑问,请展开。这个符号看起来很麻烦,而且肯定是一个品味问题。但是,不能忽略列出的概念。但是,有了一些经验,您将能够立即看到状态的哪个部分与哪种成本度量相关,例如“问题大小”或“顶点数”。其余的可以删除-这大大简化了事情!

如果您现在认为这太复杂了,建议您:!很难在任何与真实机器非常接近的模型中得出算法的确切成本,以使得能够进行运行时预测(甚至是相对的预测)也是一项艰巨的努力。而且,这甚至都没有考虑在实际计算机上进行缓存和其他令人讨厌的影响。

因此,算法分析通常简化到数学上易于处理的程度。例如,如果您不需要确切的成本,则可以在任何时候高估或低估(上限或下限):减少常量集,摆脱条件,简化总和,等等。

关于渐近成本的注记

ñ

这是(通常)公平的,因为根据机器,操作系统和其他因素,抽象语句实际上会带来一些(通常是未知的)成本,并且较短的运行时间可能由操作系统首先设置进程来决定,而并非如此。因此,无论如何,您都会感到不安。

渐近分析与这种方法的关系如下。

  1. 确定主要操作(产生成本),即最常发生的操作(取决于恒定因素)。在Bubblesort示例中,一种可能的选择是第5行的比较。

    或者,通过基本运算的最大值(从上方)约束所有常量的基本运算。他们的最小值(从下面开始)并执行通常的分析。

  2. 使用此操作的执行计数作为成本执行分析。
  3. ØΩ

Ø

进一步阅读

算法分析中还有许多挑战和技巧。这是一些推荐的读物。

围绕着使用类似技术的标记存在很多问题。


1
也许有一些参考和示例可供参考,供您参考用于定理分析的主定理(及其扩展名
Nikos M. 2014年

@NikosM这里超出范围(另请参阅上面问题的评论)。请注意,我链接到有关解决递归的参考文章,该文章确实介绍了Master theorem等。
拉斐尔

@Nikos M:我的0.02美元:虽然主定理适用于多次重复,但不会适用于其他多次重复;有解决复发的标准方法。还有一些算法,即使是运行时间,我们也不会重复出现。一些先进的计数技术可能是必要的。对于有良好数学背景的人,我建议塞奇威克和弗拉霍莱特(Sedgewick and Flajolet)的出色著作“算法分析”,其中包括“递归关系”,“生成函数”和“渐近逼近”等章节。数据结构只是偶尔出现的例子,重点是方法!
2016年

@Raphael我在网络上找不到基于操作语义的这种“将代码转换为数学”方法的内容。您能否提供更正式地处理此问题的书籍,论文或文章的参考?还是如果这是您开发的,您是否有更深入的了解?
Wyvern666 '17

1
@ Wyvern666不幸的是,没有。就任何人都可以声称自己组成这样的人来说,我是自己做的。也许我会自己写一篇引文。就是说,围绕分析组合学(Flajolet,Sedgewick等)的整个工作集是这一基础。它们大部分时间都不会为“代码”的形式语义所困扰,但是它们通常提供处理“算法”的附加成本的数学方法。老实说,我认为这里提出的概念不是很深入,不过您可以进入其中的数学。
拉斐尔

29

语句的执行计数

还有另一种方法,由Donald E. Knuth在他的《计算机编程艺术》中提出系列中。与之相反将整个算法转换为一个公式,它在“将事物放在一起”方面独立于代码的语义运行,并且仅在必要时才从“鹰眼”视图进入较低的层次。每个语句均可独立于其余语句进行分析,从而使计算更加清晰。但是,该技术非常适合于相当详细的代码,而不是更高级的伪代码。

方法

原理上很简单:

  1. 为每个语句分配一个名称/编号。
  2. 小号一世C一世
  3. 小号一世Ë一世
  4. 计算总费用

    C=一世Ë一世C一世

您可以随时插入估算值和/或符号数量,从而削弱响应能力。相应地推广结果。

Ë77Øñ日志ñ

示例:深度优先搜索

考虑以下图遍历算法:

dfs(G, s) do
  // assert G.nodes contains s
  visited = new Array[G.nodes.size]     1
  dfs_h(G, s, visited)                  2
end 

dfs_h(G, s, visited) do
  foo(s)                                3
  visited[s] = true                     4

  v = G.neighbours(s)                   5
  while ( v != nil ) do                 6
    if ( !visited[v] ) then             7
      dfs_h(G, v, visited)              8
    end
    v = v.next                          9
  end
end

{0ñ-1个}为边数。

一种CË一世

一世1个23456789Ë一世一种一种+CC-1个C

Ë8=Ë3-1个foodfsË6=Ë5+Ë7while

一种=1个foo=ñC=2

一世1个23456789Ë一世1个1个ñññ2+ñ2ñ-1个2

这导致我们的总成本为

Cñ=C1个+C2-C8+ ñC3+C4+C5+C6+C8+ 2C6+C7+C9

C一世

一世1个23456789C一世ñ001个1个01个01个

并得到

C记忆ñ=3ñ+4

进一步阅读

请参阅我的其他答案的底部。


8

像定理证明一样,算法分析在很大程度上是一门艺术(例如,有些简单的程序(例如Collat​​z问题)我们不知道如何进行分析)。我们可以将算法复杂性问题转换为数学问题,正如Raphael全面回答的那样,但是为了用已知函数表达算法成本的界限,我们不得不:

  1. 使用从现有分析中获知的技术,例如根据我们了解的重复数或求和/积分我们可以计算出的范围。
  2. 将算法更改为我们知道如何分析的算法。
  3. 提出一种全新的方法。

1
我想我看不到如何在其他答案之外添加有用的新内容。该技术已在其他答案中进行了描述。在我看来,这更像是评论,而不是问题的答案。
DW

1
我敢说其他答案证明这不是一门艺术。您可能无法做到这一点(即数学),即使您确实需要一些创造力(关于如何应用已知数学),但这对任何任务都是正确的。我认为我们不希望在这里创建新的数学。(实际上,这个问题是它的回答,旨在使整个过程变得神秘。)
拉斐尔

4
@Raphael Ari正在谈论提出一个可识别的函数作为边界,而不是“程序执行的指令数量”(这就是您的答案所要解决的问题)。一般情况一门艺术–没有一种算法可以为所有算法带来不小的限制。但是,最常见的情况是一组已知技术(例如主定理)。
吉尔斯

@Gilles如果没有算法存在的一切都是一门艺术,那么工匠(特别是程序员)的报酬会更差。
拉斐尔

1
@AriTrachlenberg提出了一个重要的观点,但是有很多方法可以评估算法的时间复杂度。大O标记定义本身暗示或直接陈述了其理论性质(取决于作者)。“最坏的情况”显然为猜想和/或新事实提供了讨论的余地。更不用说渐进估计的本质了……不精确。
布赖恩·奥格登
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.