算法和算法评价
1 算法的基本概念
算法(Algorithm)是对特定问题求解步骤的一种描述,它是指令的有限序列,其中的每条指令表示一个或多个操作。一般具有下列5个重要特性:
- 有穷性:一个算法必须在执行有穷步之后结束,且每一步都可在有穷时间内完成。
- 确定性:算法中每条指令必须有确切的含义,对于相同的输入只能得到相同的输出。
- 可行性:算法中描述的操作都可以通过已经实现的基本运算执行有限次来实现。
- 输入:一个算法有零个或多个输入,这些输入取自于某个特定的对象的集合。
- 输出:一个算法有一个或多个输出,这些输出是与输入有着某种特定关系的量。
2 算法效率的度量
算法效率的度量是通过时间复杂度和空间复杂度来描述的。
2.1 算法运行时间的估算
算法执行时间需通过依据该算法编制的程序在计算机上运行时所消耗的时间来度量,而度量一个程序的执行时间通常有两种方法:
- 事后统计的方法:先运行一次程序,然后测量算法程序的运行时间。这种方法非常直接,也有一些明显的缺陷:一是必须先运行一次程序,如果程序运行时间很长,就会消耗大量时间;二是依赖于特定计算机的运行速度,依赖于算法程序实现的质量,依赖于编译器编译的效率,有太多的无关因素影响度量标准,因此人们常常采用事前分析估算的方法。
- 事前分析估算的方法:一个用高级程序语言编写的程序在计算机上运行时所消耗的时间取决于下列因素:
- 依据的算法选用何种策略
- 问题的规模
- 书写程序的语言,对于同一个算法,语言越低级,执行效率就越高
- 编译程序所产生的机器代码的质量
- 机器执行指令的速度
我们可以发现,同一个算法在用不同的语言实现,或者用不同的编译程序编译,或者是在不同计算机上运行时,效率都会不同。这说明用绝对的时间来衡量算法的效率是不合适的。我们希望找到一个不依赖与这些无关因素的度量标准。在上述5个条件中抛开与外部环境有关的因素,我们可以认为一个特定算法“运行工作量”的大小,只依赖于问题的规模。
对于一个规模固定的问题,我们可以尝试去统计算法每一步操作的执行次数,用这个指标来衡量算法的效率。实际操作后,我们会发现这种做法过于困难,而且常常是没有必要的。我们应该做的是找出算法中最重要的操作,即所谓的基本操作(Baisc Operation),它们对总运行时间的贡献最大,然后计算它们的运行次数。根据以上原则,我们就不难发现一个算法中的基本操作往往是算法最内层循环中最费时的操作。例如,大部分排序算法是通过比较待排序的元素和交换元素这两步来工作的,对于这类算法,比较和交换就是基本操作。再比如,我们想用一个for循环重复累加来计算数列 { 1 , 2 , 3 , … , 100 } \{1, 2, 3, \dots, 100\} {1,2,3,…,100}的和,加法运算就是这个算法的基本操作。
这样我们就建立起一个分析算法时间效率的框架:对于输入规模为 n n n的算法,统计它的基本操作执行次数,来对其效率进行度量。
我们约定
c
o
p
c_{op}
cop为特定计算机上一个算法基本操作的执行时间,而
C
(
n
)
C(n)
C(n)是该算法需要执行基本操作的次数。这样,对运行在特定计算机上的算法程序的运行时间可以用下列公式计算:
T
(
n
)
≈
c
o
p
C
(
n
)
T(n) \approx c_{op}C(n)
T(n)≈copC(n)
2.2 算法的最优、最差和平均效率
我们在上一节提到以算法输入规模为参数的函数可以合理地度量算法的效率。但是也有许多算法的运行时间不仅取决于输入的规模,还取决于特定的输入细节。例如,我们用下列算法来查找一个包含 n n n个元素的数组中大小为 K K K的元素:
算法 SequentialSearch(A[0, 1, ..., n-1], K)
// 用顺序查找在给定的数组中查找给定的值
// 输入:数组A[0, 1, ..., n-1]和查找键K
// 输出:返回第一个匹配K的元素的下标,如果没有匹配元素则返回-1
i ← 0
while i < n and A[i] ≠ K do
i ← i +1
if i < n return i
else reurn -1
假设我们数组中没有我们要找的元素 K K K,或是元素 K K K在数组的最后一位,我们就需要遍历完整个数组;假设数组第一位就是我们要找的元素 K K K,那么算法只需要执行一步。 很明显,对于相同规模的数组,算法的运行时间也会产生很大的差异。
- 最差效率 (Worst-case Efficiency):指当输入规模为 n n n时算法在最坏情况下的效率。分析算法的最差效率有助于我们了解算法运行时间的上界,换句话说,在任何情况下,算法的运行时间不会超过最坏输入情况下的运行时间 C w o r s t ( n ) C_{worst}(n) Cworst(n)。
- 最优效率 (Best-case Efficiency):指当输入规模为 n n n时算法在最优情况下的效率。最优效率的分析不如最差效率分析重要,但是它也不是毫无用处。有些算法对于一些接近最优输入的有用输入类型,也可以获得类似最优效率的良好性能。因此我们可以对输入数据进行刻意挑选来获得算法更好的性能。其次,如果一个算法的最优效率都不能满足要求,我们就可以立刻放弃对该算法的研究,不必进一步分析。
- 平均效率 (Average-case Efficiency):无论是最差还是最优效率,都不能体现出在“随机”情况下算法的效率,因此我们还需要平均效率。
2.3 渐进符号和基本效率类型
在上文我们用 T ( n ) T(n) T(n)或 t ( n ) t(n) t(n)来表示算法的运行时间, C ( n ) C(n) C(n)来表示基本操作的次数,现在我们加入 g ( n ) g(n) g(n)来表示和基本操作次数相比较的函数。
2.3.1 符号 O O O
如果存在大于0的常数 c c c和非负的整数 n 0 n_0 n0,使得对于所有的 n ≥ n 0 n \ge n_0 n≥n0,有 t ( n ) ≤ c g ( n ) t(n) \le cg(n) t(n)≤cg(n),则说函数 t ( n ) t(n) t(n)包含在 O ( g ( n ) ) O(g(n)) O(g(n))中,记作 t ( n ) ∈ O ( g ( n ) ) t(n) \in O(g(n)) t(n)∈O(g(n))
非正式来说,
O
(
g
(
n
)
)
O(g(n))
O(g(n))是增长次数小于等于
g
(
n
)
g(n)
g(n)的函数集合,例如,我们可以说
100
n
+
5
∈
O
(
n
2
)
100n+5 \in O(n^2)
100n+5∈O(n2)。
2.3.2 符号 Ω \Omega Ω
如果存在大于0的常数 c c c和非负的整数 n 0 n_0 n0,使得对于所有的 n ≥ n 0 n \ge n_0 n≥n0,有 t ( n ) ≥ c g ( n ) t(n) \ge cg(n) t(n)≥cg(n),则说函数 t ( n ) t(n) t(n)包含在 Ω ( g ( n ) ) \Omega(g(n)) Ω(g(n))中,记作 t ( n ) ∈ Ω ( g ( n ) ) t(n) \in \Omega(g(n)) t(n)∈Ω(g(n))
Ω ( g ( n ) ) \Omega(g(n)) Ω(g(n))是增长次数大于等于 g ( n ) g(n) g(n)的函数集合,例如 n 3 ∈ Ω ( n 2 ) n^3 \in \Omega(n^2) n3∈Ω(n2)
2.3.3 符号 Θ \Theta Θ
如果存在大于0的常数 c 1 c_1 c1, c 2 c_2 c2和非负的整数 n 0 n_0 n0,使得对于所有的 n ≥ n 0 n \ge n_0 n≥n0,有 c 2 g ( n ) ≤ t ( n ) ≤ c 1 g ( n ) c_2g(n) \le t(n) \le c_1g(n) c2g(n)≤t(n)≤c1g(n),则说函数 t ( n ) t(n) t(n)包含在 Θ ( g ( n ) ) \Theta(g(n)) Θ(g(n))中,记作 t ( n ) ∈ Θ ( g ( n ) ) t(n) \in \Theta(g(n)) t(n)∈Θ(g(n))
Θ
(
g
(
n
)
)
\Theta(g(n))
Θ(g(n))是增长次数等于
g
(
n
)
g(n)
g(n)的函数集合。例如,
1
2
n
(
n
−
1
)
∈
Θ
(
n
2
)
\frac{1}{2}n(n-1) \in \Theta(n^2)
21n(n−1)∈Θ(n2)。
2.3.4 渐进符号的特性
根据渐进符号的正式定义,我们可以得到下列定理:
一、加法规则
如果 t 1 ( n ) ∈ O ( g 1 ( n ) ) t_1(n) \in O(g_1(n)) t1(n)∈O(g1(n))并且 t 2 ( n ) ∈ O ( g 2 ( n ) ) t_2(n) \in O(g_2(n)) t2(n)∈O(g2(n)),则
t 1 ( n ) + t 2 ( n ) ∈ O ( m a x g 1 ( n ) , g 2 ( n ) ) t_1(n) +t_2(n) \in O(max{g_1(n), g_2(n)}) t1(n)+t2(n)∈O(maxg1(n),g2(n))
二、乘法规则
如果 t 1 ( n ) ∈ O ( g 1 ( n ) ) t_1(n) \in O(g_1(n)) t1(n)∈O(g1(n))并且 t 2 ( n ) ∈ O ( g 2 ( n ) ) t_2(n) \in O(g_2(n)) t2(n)∈O(g2(n)),则
t 1 ( n ) × t 2 ( n ) ∈ O ( g 1 ( n ) ) × O ( g 2 ( n ) ) ∈ O ( g 1 ( n ) × g 2 ( n ) ) t_1(n) \times t_2(n) \in O(g_1(n)) \times O(g_2(n)) \in O(g_1(n) \times g_2(n)) t1(n)×t2(n)∈O(g1(n))×O(g2(n))∈O(g1(n)×g2(n))
(对于符号 Ω \Omega Ω和 Θ \Theta Θ,类似的断言也成立。)
这些特性意味着算法的整体效率是由具有较大增长次数的部分所决定的,即它效率较差的那一部分。例如,我们使用下述这个两部分算法来检查数组中是否含有相等元素:1. 应用某种已知的排序算法对数组进行排序, 2. 扫描该有序数组,比较相邻元素是否相等。假设第一部分的比较次数不会超过 1 2 n ( n − 1 ) \frac{1}{2}n(n-1) 21n(n−1)(属于 O ( n 2 ) O(n^2) O(n2)),而算法的第二部分比较次数不超过 n − 1 n-1 n−1 (因此属于 O ( n ) O(n) O(n)),该算法的整体效率应该属于 O ( m a x { n 2 , n } ) = O ( n 2 ) O(max \{ n^2, n \}) = O(n^2) O(max{n2,n})=O(n2)。
2.4 时间复杂度
在了解了渐进符号后,我们就可以来定义时间复杂度了。
一个语句的频度是指该语句在算法中被重复执行的次数。我们通常将算法中所有语句的频度之和记为
T
(
n
)
T(n)
T(n),它是该算法问题规模
n
n
n的函数,时间复杂度主要分析
T
(
n
)
T(n)
T(n)的数量级。因为在算法中基本操作的频度和
T
(
n
)
T(n)
T(n)在同一个数量级,所以我们使用算法中基本操作的频度
f
(
n
)
f(n)
f(n)来分析算法的时间复杂度:
T
(
n
)
=
O
(
f
(
n
)
)
T(n) = O(f(n))
T(n)=O(f(n))
没有特殊说明的情况下,我们说的时间复杂度都是指最差时间复杂度。如果一个算法的时间复杂度
T
(
n
)
=
O
(
f
(
n
)
)
T(n) = O(f(n))
T(n)=O(f(n)),那么我们说该算法的问题规模为
n
n
n,其执行时间和
f
(
n
)
f(n)
f(n)成正比。
时间复杂度由问题规模
n
n
n和输入的初始状态共同决定。
2.5 空间复杂度
算法的空间复杂度
S
(
n
)
S(n)
S(n)定义为该算法所耗费的存储空间,它是问题规模
n
n
n的函数。记为:
S
(
n
)
=
O
(
g
(
n
)
)
S(n) = O(g(n))
S(n)=O(g(n))
算法原地工作是指算法所需的辅助空间为常量,即
O
(
1
)
O(1)
O(1)。
相关章节
第一节 【绪论】数据结构的基本概念
第二节 【绪论】算法和算法评价
第三节 【线性表】线性表概述
第四节 【线性表】线性表的顺序表示和实现
第五节 【线性表】线性表的链式表示和实现
第六节 【线性表】双向链表、循环链表和静态链表
第七节 【栈和队列】栈
第八节 【栈和队列】栈的应用
第九节 【栈和队列】栈和递归
第十节 【栈和队列】队列