对于评估一个算法的实用性,一个重要的结论关系到运行这个算法所花费的时间。典型地,对一个特殊的函数,我们不可以确信能找到最有效的算法来描述。但大多 数情况下,我们可以用一个特殊的数量级来表示执行一个特殊算法所付出的量。因此,我们可以通过与当前或预知处理器的速度相比较,来决定一个特殊算法的实用 性等级。
算法时间复杂性是衡量算法有效性的常用标准。如果我们用f(n)来表示时间复杂度,那么对所有的n和长度为n的输入,执行这个算法就要至多进行f(n)步。因此,对于给定大小的输入和给定的处理器速度,时间复杂度就是执行时间的最大值。
这里存在着几个不明确的地方。首先,步的定义不精确,一步可以是一个单一的图灵机操作,一条单一处理器指令,也可以是一条单一的高级语言机器指令等等。然而,这些不同的步定义都可以由简单的乘法常数联系起来。对于非常大的n值,这些常数是不重要的,重要的是相对执行时间增长多快。
第二个问题,总的来说,我们不能固定一个精确的公式来表示f(n),而只能近似地表示。但我们首先感兴趣的是,当n变得非常大时,f(n)的变化率。常用一个标准数学记法,也就是称为的“大O”表示法来表征算法的时间复杂度。定义为:
,
当且仅当存在两个数a和M时,
(1)
下面是一个例子来帮助阐明这个表示法的使用。假定我们希望计算下述的多项式:
考虑以下[POHL81]给出的简单算法:
算法 P1;
n, i, j: integer; x, polyval: real;
a, S: array [0..100] of real;
begin
read(x, n);
for i := 0 upto n do
begin
S[i] := 1; read(a[i]);
for j := 1 upto i do S[i] := x ×S[i];
S[i] := a[i] ×S[i]
end;
polyval := 0;
for i := 0 upto n do polyval := polyval + S[i];
write ('value at', x, 'is', polyval)
end.
该算法分别计算每个子表达式的值。每一个S[i]需要(i+1)次乘法:i次乘法来计算S[i]和一次来乘a[i]。计算所有的n项需要
次乘法。还有(n+1)次的加法,不过相对于大量的乘法,我们可以忽略这些加法次数。因此,这个算法的时间复杂度是 。
现在,我们来证明。从等式(1)的定义,我们应该知道,对于a=1和M=4,关系式中,这是我们通过对n的归纳得出的。由于,关系式中n=4。现在假设对关系式对所有小于或等于k值的n都成立 【 如】。那么对n=k+1:
因此,对n=k+1时,结论成立。
通常,大O表示法使用增长速度最快项,例如:
⒈
⒉
⒊
其实大O表示法还有许多其他意义,有兴趣的读者,请参阅文献[GRAH94]和[KNUT97]。
对于输入大小为n的一个算法有以下几种说法:
-
线性的:如果运行时间为
-
多项式的:如果运行时间为,t为一常数
-
指数的:如果运行时间为,t为常数,h(n)为多项式
通常,在多项式时间内能够被解决的问题被认为是可行的,然而,任何大于多项式性时间的,特别是指数性时间的,被认为是不可行的。但是你必须注意这些术语。首先,如果输入值足够小,甚至非常复杂的算法都认为是可行的。例如,假定你有一个单位时间可以执行次操作的系统,表格1表明了对不同的复杂性的算法,单位时间内可处理的输入规模大小。对于指数性或阶乘性时间的算法,仅仅只有非常小的输入时才能适应。
第二个应该注意的是输入的特征性。例如,对一个加密算法解密时的复杂性是具有特征性的,可根据一些可能的密钥数字或者密钥的长度。例如对于高级加密标准(AES),可能的密钥数字是,密钥的长度是128bits。如果我们认为一次加密是一个“步骤”,可能的密钥数字是,那么根据密钥数字来说,算法的复杂性是线性的,但是根据密钥的长度来说,复杂性又是指数性的了。
表格1 不同等级复杂性的付出代价等级
复杂性 | 输入大小 | 操作数 | ||
|
|
| ||
|
|
| ||
|
|
| ||
|
|
| ||
|
|
| ||
|
|
|
本文是翻译的William Stallings的《The Complexity of Algorithms》。
有错误的地方请指正,谢谢!