如何描述算法,证明和分析算法?


20

在阅读计算机编程艺术(TAOCP)之前,我没有深入考虑这些问题。我将使用伪代码来描述算法,理解算法并仅估计增长顺序的运行时间。该TAOCP彻底改变了我的脑海里。

TAOCP使用英语加上步骤和goto来描述算法,并使用流程图更轻松地描绘算法。似乎是低级的,但是我发现有一些优点,尤其是流程图,我已经忽略了很多。我们可以在计算遍历该箭头时用关于当前事务状态的断言标记每个箭头,并为该算法提供归纳证明。作者说:

作者的争论是我们真正理解为什么只有当我们达到了我们的思想隐含地填充所有断言的程度时算法才是有效的方法,如图4所示。

我还没有经历过这样的东西。另一个优点是,我们可以计算每个步骤执行的次数。用基尔霍夫的第一定律很容易检查。我没有精确地分析运行时间,因此在估算运行时间时可能会省略一些。±1

分析增长顺序有时是无用的。例如,由于它们都是,因此我们无法区分quicksort和heapsort ,其中是期望的随机变量,因此我们应该分析常数,例如,和,因此我们可以比较和更好。而且,有时我们应该比较其他数量,例如方差。仅对运行时间的增长顺序进行粗略分析是不够的。作为TAOCPE(T(n))=Θ(nlogn)EXXE(T1(n))=A1nlgn+B1n+O(logn)E(T2(n))=A2lgn+B2n+O(logn)T1T2 将算法翻译成汇编语言并计算运行时间,这对我来说太难了,所以我想知道一些技术来更粗略地分析运行时间,这对于C,C ++等高级语言也很有用或伪代码。

我想知道研究工作中主要使用哪种描述风格,以及如何处理这些问题。


6
仔细比较算法的执行时间时,您应该非常小心。实际的计算机具有高速缓存,寄存器和管线,它们可以大大改变运行时间。如果要找出哪种算法实际上更快,则必须在计算机上实际运行它。
svick 2012年

1
事实上,分析汇编如克努特用途的方式相比,因为没有什么是隐藏和控制流很容易分析现实生活中的代码更容易。你是要练习;我认为戴夫的评论适用。与进行严格的分析相比,从业人员更可能使用运行时测量来设计其算法。但是,我不是从业者,所以请一言以蔽之。
拉斐尔

1
@Raphael My 在实践中是指在实践中从事研究工作,而不是编程
Yai0Phah 2012年

@Frank,你是什么意思方差?我的性能测试给我时间上的差异。
edA-qa mort-ora-y 2012年

@Raphael,您的第一点不再是真的。现代芯片对您的装配进行重新排序,无序存储/加载,并预测运行和加载。对于并发以及先前的问题,实际上需要进行全面的分析,但是我并没有以正式的形式进行分析。
edA-qa mort-ora-y 2012年

Answers:


18

有许多可行的方法。哪个最适合取决于

  • 什么,你都试图表明,
  • 您想要或需要多少细节

如果该算法是众所周知的算法,并且您将其用作子例程,则通常会处于较高级别。如果算法是研究的主要对象,则可能需要更详细地说明。对于分析也可以这样说:如果您需要一个大致的运行时上限,则与需要精确的语句计数时的处理方法不同。

我将为您提供三个著名算法Mergesort的示例,这些示例有望说明这一点。

高水平

Mergesort算法获取一个列表,将其分成两个(大约)等长的部分,在这些部分列表上递归,然后合并(排序的)结果,以便对最终结果进行排序。在单例或空列表上,它返回输入。

该算法显然是正确的排序算法。拆分列表并合并列表可以分别在时间,这使我们可以重现最坏情况下的运行时。根据Master定理,这等于。T n = 2 T nΘ(n)TT(n)=2T(n2)+Θ(n)T(n)Θ(nlogn)

中级

Mergesort算法由以下伪代码给出:

procedure mergesort(l : List) {
  if ( l.length < 2 ) {
    return l
  }

  left  = mergesort(l.take(l.length / 2)
  right = mergesort(l.drop(l.length / 2)
  result = []

  while ( left.length > 0 || right.length > 0 ) {
    if ( right.length == 0 || (left.length > 0 && left.head <= right.head) ) {
      result = left.head :: result
      left = left.tail
    }
    else {
      result = right.head :: result
      right = right.tail
    }
  }

  return result.reverse
}

通过归纳证明正确性。对于长度为零或一的列表,该算法非常正确。作为归纳假设,对于某些任意但固定的自然,假设mergesort在长度最大为列表上正确执行。现在让为长度为的列表。通过归纳假设,并保持(递减)排序的第一个响应版本。递归调用后后半部分。因此,循环在每次迭代中选择尚未调查的最小元素,并将其附加到;因此是一个非递增排序的列表,其中包含和中的所有元素n > 1 公升nn>1L大号大号n+1leftrightLwhileresultresultleftright。反向是的非递减排序版本,它是返回的-期望的-结果。L

对于运行时,让我们计算元素比较和列表操作(渐近地主导运行时)。长度小于2的列表均不会导致这种情况。对于长度为列表,我们进行了一些操作,这些操作是通过为递归调用准备输入而引起的,这些操作来自递归调用本身加上循环和one 。两个递归参数每个最多可以使用列表操作来计算。该循环正好执行了次,每次迭代最多导致一个元素比较和正好两个列表操作。最终可以实现使用n n 2 nn>1whilereversenwhilenreverse2n列表操作-将每个元素从输入中删除并放入输出列表中。因此,操作计数满足以下重复发生:

T(0)=T(1)=0T(n)T(n2)+T(n2)+7n

由于明显不减小,因此对于渐近增长,考虑就足够了。在这种情况下,重复发生简化为n = 2 kTn=2k

T(0)=T(1)=0T(n)2T(n2)+7n

通过Master定理,我们得到,它扩展到的运行时。TΘ(nlogn)mergesort

超低水平

考虑一下Isabelle / HOL中的Mergesort的(广义)实现:

types dataset  =  "nat * string"

fun leq :: "dataset \<Rightarrow> dataset \<Rightarrow> bool" where
   "leq (kx::nat, dx) (ky, dy) = (kx \<le> ky)"

fun merge :: "dataset list \<Rightarrow> dataset list \<Rightarrow> dataset list" where
"merge [] b = b" |
"merge a [] = a" |
"merge (a # as) (b # bs) = (if leq a b then a # merge as (b # bs) else b # merge (a # as) bs)"

function (sequential) msort :: "dataset list \<Rightarrow> dataset list" where
  "msort []  = []" |
  "msort [x] = [x]" |
  "msort l   = (let mid = length l div 2 in merge (msort (take mid l)) (msort (drop mid l)))"
by pat_completeness auto
  termination
  apply (relation "measure length")
by simp+

这已经包括明确定义和终止的证明。在此处找到(几乎)完整的正确性证明。

对于“运行时”,即比较次数,可以设置与上一节中类似的重复。除了使用Master定理并忽略常量之外,您还可以对其进行分析,以获得近似渐近等于真实量的近似值。您可以在[1]中找到完整的分析;这是一个粗略的轮廓(不一定适合Isabelle / HOL代码):

如上所述,比较次数的重复次数为

f0=f1=0fn=fn2+fn2+en

其中是合并部分结果所需的比较次数²。为了摆脱地板和天花板,我们对是否为偶数进行了区分: ñenn

{f2m=2fm+e2mf2m+1=fm+fm+1+e2m+1

使用和嵌套前向/后向差异,我们得到fnen

k=1n1(nk)Δfk=fnnf1

该和与Perron公式的右边匹配。我们定义狄氏产生串联的为Δfk

W(s)=k1Δfkks=112sk1Δekks=: (s)

加上Perron的公式,我们可以

fn=nf1+n2πi3i3+i(s)ns(12s)s(s+1)ds

评价取决于被分析这种情况下。除此之外,我们可以-经过一些欺骗之后-应用残差定理来获得(s)

fnnlog2(n)+nA(log2(n))+1

其中是周期函数,其值在。A[1,0.9]


  1. 梅林变换和渐近性:Flajolet和Golin(1992)的mergesort重现
  2. 最佳情况: 最坏情况: 平均情况:ËÑ=ñ-1个Ëñ=ñ-en=n2
    en=n1
    en=nn2n2+1n2n2+1

我的运行时分析问题是,如何准确确定和,这很接近实践(例如,可以比较merge-sort和qsort)。β Ť Ñ = Ť ñ / 2 + Ť ñ / 2 + α Ñ + βαβT(n)=T(n/2)+T(n/2)+αn+β
Yai0Phah 2012年

@弗兰克:简短的答案是你不能;常量取决于实现细节(包括机器体系结构,语言和编译器),而这些细节对于基础算法而言并不重要。
JeffE 2012年

@JeffE我必须声明和仅应足够精确才能进行一些比较。更简单地说,一个数学模型可以完成许多工作而无需使用机器语言来确定常数。βαβ
Yai0Phah 2012年

以@JeffE为例,taocp中的MIX / MMIX是,但是将算法转换成这种机器语言太难了。
Yai0Phah 2012年

@FrankScience:为了接近实践,您必须计算所有操作(如Knuth一样)。然后,您可以使用特定于机器的操作成本实例化结果,以获得真实的运行时间(忽略操作顺序可能具有的影响,缓存,管线等)。通常,人们只计算一些操作,在这种情况下,固定和不会告诉您太多信息。βαβ
拉斐尔

3

迪杰斯特拉(Dijkstra)的“编程学科”就是关于分析和证明算法以及为可证明性进行设计。在该书的序言中,Dijkstra解释了一种经过适当设计以进行分析的非常简单的构造型迷你语言如何足以正式解释许多算法:

当开始这样的书时,会立即面临一个问题:“我将使用哪种编程语言?”,而这不是只是一个介绍问题!任何工具最重要但也是最难以捉摸的方面是它对那些训练使用它的人的习惯的影响。如果该工具是一种编程语言,那么无论我们是否喜欢,这种影响都会对我们的思维习惯产生影响。据我所知,对这种影响进行了分析,得出的结论是,现有的编程语言或它们的子集都不适合我的目的。另一方面,我知道自己还没有为新的编程语言设计做准备,以至于我发誓在接下来的五年中不要这样做,而且我有一个最明显的感觉,那就是那段时期还没有过去!(在此之前,必须撰写本专着。

后来,他解释了自己如何获得了迷你语言。

我欠读者一个解释,为什么我将迷你语言保持得如此之小以至于它甚至不包含过程和递归。…… 重点是,我认为不需要它们来传达我的信息,即。精心选择的关注点分离对于设计各个方面的高质量程序是至关重要的;迷你语言的适度工具已经为我们提供了足够的自由度,可以进行非平凡但非常令人满意的设计。

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.