将代码翻译成数学
给定(或多或少)形式化的操作语义,您可以将算法的(伪)代码从字面上转换为可以为您提供结果的数学表达式,前提是您可以将表达式操纵为有用的形式。这对于附加成本度量(例如比较数,交换,语句,内存访问,某些抽象机需求的循环等等)非常有效。
示例: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(n)=∑i=0n−2∑j=0n−i−21=⋯=n(n−1)2=(n2)
其中是第5行(我们计算)的每次执行费用。1
示例:在Bubblesort中交换
我将表示,它由线的子程序,以和由Ç 我,Ĵ用于执行该子程序(一次)的费用。P我,Ĵi
j
C我,Ĵ
现在,让我们说,我们要算掉,那是多么经常执行。这是一个“基本块”,它是一个子程序,始终以原子方式执行并具有一定的成本(此处为1)。压缩这样的块是一种有用的简化,我们经常在不考虑或谈论它的情况下就应用它。P6 ,81个
通过与上述类似的翻译,我们得出以下公式:
。C掉期(A )= ∑我= 0n − 2∑j = 0n − i − 2C5,9(A(i,j))
表示前阵列的状态(我,Ĵ )的次迭代 P 5 ,9。A(i,j)(i,j)P5,9
请注意,我使用而不是n作为参数。我们很快就会明白原因。我不加我和Ĵ作为参数Ç 5 ,9,因为成本不依赖于他们在这里(在统一的成本模型,这是); 一般来说,他们只是可能。AnijC5,9
显然,成本取决于内容甲(值和,特别是),所以我们必须考虑到这一点。现在我们面临一个挑战:我们如何“解包” Ç 5 ,9?好吧,我们可以使对A内容的依赖关系明确:P5 ,9一种A[j]
A[j+1]
C5 ,9一种
。C5 ,9(一(i ,j ))= C5(一(i ,j ))+ { 10,一(i ,j )[ j ] > A(i ,j )[ j + 1 ],否则
对于任何给定的输入数组,这些成本是明确定义的,但是我们需要更笼统的说明;我们需要做出更强有力的假设。让我们研究三种典型的情况。
最坏的情况
刚刚从看总和并注意到,我们可以找到一个微不足道的上限费用:C5 ,9(一(i ,j ))∈ { 0 ,1 }
。C掉期(甲)≤ Σ我= 0n − 2∑j = 0n − i − 21 = n (n − 1 )2= ( n2)
但是会发生这种情况吗,即达到该上限的?事实证明,是的:如果我们输入成对的不同元素的反向排序数组,则每次迭代都必须执行一次交换¹。因此,我们得出了Bubblesort交换的最坏情况的确切数字。一种
最好的情况
相反,有一个小的下限:
。C掉期(甲)≥ Σ我= 0n − 2∑j = 0n − i − 20 = 0
这也可能发生:在已排序的数组上,Bubblesort不会执行单个交换。
平均情况
最坏的情况和最好的情况还存在很大差距。但是掉期的典型数量是多少?为了回答这个问题,我们需要定义“典型”的含义。从理论上讲,我们没有理由偏爱一个输入而不是另一个输入,因此我们通常假定所有可能输入的分布均匀,即每个输入均具有相同的可能性。我们将自己限制为具有成对的不同元素的数组,因此采用随机排列模型。
然后,我们可以像这样重写我们的费用²:
è [ Ç掉期] = 1n !∑一种∑我= 0n − 2∑j = 0n − i − 2C5 ,9(一(i ,j ))
现在我们必须超越简单的求和运算。通过查看算法,我们注意到,每一个交换去除只有一个倒置在(我们永远只能换neighbours³)。也就是说,在执行交换次数甲正是反转次数INV (甲)的甲。因此,我们可以替换内部的两个和得到一种一种v(一)一种
。è [ Ç掉期] = 1n !∑一种v(一)
对我们来说幸运的是,平均反演次数已确定为
è [ Ç掉期] = 12⋅ ( n2)
这是我们的最终结果。请注意,这恰好是最坏情况下成本的一半。
- 请注意,算法是经过精心制定的,因此
i = n-1
不会执行从未执行任何操作的外部循环的“最后一次”迭代。
- “ ”是“期望值”的数学符号,这里只是平均值。Ë
- 我们沿途学习,没有一种仅交换相邻元素的算法在渐近性上可以比Bubblesort渐近地快(甚至是平均水平)-求反的次数是所有此类算法的下限。这适用于例如插入排序和选择排序。
通用方法
在示例中我们已经看到,我们必须将控制结构转换为数学。我将介绍翻译规则的典型合奏。我们还已经看到,任何给定子程序的成本可能取决于当前状态,即(大致而言)变量的当前值。由于该算法(通常)会修改状态,因此通用方法要注释起来有点麻烦。如果您开始感到困惑,建议您回到该示例或自己编写示例。
我们用表示当前状态(将其想象为一组变量赋值)。当我们执行从状态ψ开始的程序时,我们以状态ψ / P结束(提供终止)。ψP
ψψ / PP
个人陈述
仅给出一个语句S;
,就将其分配为。这通常是一个常数函数。C小号(ψ )
表达方式
如果您具有E
形式的表达式E1 ∘ E2
(例如,算术表达式,其中∘
可能是加法或乘法),则需要递归累加成本:
。CË(ψ )= c∘+ CË1个(ψ )+ CË2(ψ )
注意
- 操作成本可以不是恒定的,而是依赖于值ë 1和ë 2和C∘Ë1个Ë2
- 评估表达式可能会改变许多语言的状态,
因此您可能必须对这个规则保持灵活。
序列
给定一个程序P
作为程序序列Q;R
,您将成本添加到
。CP(ψ )= C问(ψ )+ C[R(ψ / Q)
有条件的
给定P
形式的程序if A then Q else R end
,费用取决于状态:
CP(ψ )= C一种(ψ )+ { C问(ψ / A)C[R(ψ / A), 在ψ 下A为真 ,否则
通常,评估A
可能会很好地改变状态,因此会更新各个分支机构的成本。
循环
给定P
形式的程序for x = [x1, ..., xk] do Q end
,分配成本
CP(ψ )= cinit_for+ ∑我= 1ķCstep_for+ C问(ψ一世∘ { x := x i })
其中是处理前的状态为值与迭代之后,即,被设置为,..., 。ψ一世Q
xi
x
x1
xi-1
注意用于循环维护的额外常量;必须创建循环变量()并为其赋值(c step_for)。这很重要,因为Cinit_forCstep_for
- 计算下一个
xi
可能会很昂贵,并且
for
如果执行迭代,则具有空主体的-loop(例如,在使用最佳成本的最佳情况下进行简化后)不会具有零成本。
循环
给定P
形式的程序while A do Q end
,分配成本
CP(ψ ) = C一种(ψ )+ { 0C问(ψ / A)+ CP(ψ / A ; Q), 在ψ 下A为假 , 否则
通过检查算法,这种重复经常可以很好地表示为类似于for循环的总和。
示例:考虑以下简短算法:
while x > 0 do 1
i += 1 2
x = x/2 3
end 4
通过应用规则,我们得到
C1 ,4({ i := i0; x := x0} ) = c<+ { 0C+ =+ c/+ C1 ,4({ i := i0+ 1 ; X := ⌊ X0/ 2 ⌋ } ),X0≤ 0, 否则
C…i
x
C1 ,4i
C1 ,4(x )= { c>C>+ c+ =+ c/+ C1 ,4(⌊ X / 2 ⌋ ),X ≤ 0, 否则
这解决了基本的手段来
C1 ,4(ψ )= ⌈ 日志2ψ (X )⌉ &CenterDot;& (Ç>+ c+ =+ c/)+ c>
ψ = { … ,x := 5 ,… }ψ (x )= 5
程序调用
给定某个参数P
形式的程序,其中是带有(命名)参数的过程,请分配费用M(x)
x
M
p
CP(ψ )= c呼叫+ C中号(ψ球状∘ { p := x } )
C呼叫ψ
我掩盖了您可能会对此处的状态产生的一些语义问题。您将要区分全局状态和过程调用的局部状态。假设我们仅在此处传递全局状态,并M
获得一个新的局部状态,方法是将的值设置p
为x
。此外,x
可能是一个表达式,我们(通常)假定在传递它之前先对其求值。
示例:考虑程序
fac(n) do
if ( n <= 1 ) do 1
return 1 2
else 3
return n * fac(n-1) 4
end 5
end
根据规则,我们得到:
C事实({ n := n0} )= C1 ,5({ n := n0} )= c≤+ { C2({ n := n0} )C4({ n := n0} ),n0≤ 1, 否则= c≤+ { c返回C返回+ c∗+ c呼叫+ C事实({ n := n0− 1 } ),n0≤ 1, 否则
请注意,我们无视全局状态,因为fac
显然不访问任何状态。这种特定的复发很容易解决
C事实(ψ )= ψ (n )⋅ (c≤+ c返回)+ (ψ (n )− 1 )⋅ (c∗+ c呼叫)
我们已经介绍了典型伪代码中将遇到的语言功能。分析高级伪代码时要注意隐藏的成本;如有疑问,请展开。这个符号看起来很麻烦,而且肯定是一个品味问题。但是,不能忽略列出的概念。但是,有了一些经验,您将能够立即看到状态的哪个部分与哪种成本度量相关,例如“问题大小”或“顶点数”。其余的可以删除-这大大简化了事情!
如果您现在认为这太复杂了,建议您:是!很难在任何与真实机器非常接近的模型中得出算法的确切成本,以使得能够进行运行时预测(甚至是相对的预测)也是一项艰巨的努力。而且,这甚至都没有考虑在实际计算机上进行缓存和其他令人讨厌的影响。
因此,算法分析通常简化到数学上易于处理的程度。例如,如果您不需要确切的成本,则可以在任何时候高估或低估(上限或下限):减少常量集,摆脱条件,简化总和,等等。
关于渐近成本的注记
ñ
这是(通常)公平的,因为根据机器,操作系统和其他因素,抽象语句实际上会带来一些(通常是未知的)成本,并且较短的运行时间可能由操作系统首先设置进程来决定,而并非如此。因此,无论如何,您都会感到不安。
渐近分析与这种方法的关系如下。
确定主要操作(产生成本),即最常发生的操作(取决于恒定因素)。在Bubblesort示例中,一种可能的选择是第5行的比较。
或者,通过基本运算的最大值(从上方)约束所有常量的基本运算。他们的最小值(从下面开始)并执行通常的分析。
- 使用此操作的执行计数作为成本执行分析。
- ØΩ
Ø
进一步阅读
算法分析中还有许多挑战和技巧。这是一些推荐的读物。
围绕着使用类似技术的标记算法分析存在很多问题。