专题1 算法分析技术
一、算法(Algorithm)
在正式介绍算法的时间复杂度之前,首先抛出一个问题:算法是什么?
顾名思义,算法就是计算的方法。更严谨地说,算法应该是求解问题的途径。 现实生活中,往往会利用计算机求解各种问题,而这些问题无外乎两个要素:问题描述+输入、输出。算法就是计算机求解问题的具体过程,它包含了若干计算机能够理解的指令,对于问题的每一个输入,总是给出正确的输出,即问题的解。
下图给出了算法与问题的关系,以便于理解:
Pascal语言之父Nicklaus Wirth用一句非常经典的话,概括了算法在程序设计中的核心地位。希望大家能以这句话作为开端,在今后学习更多有趣的算法:
程序=算法+数据结构 --Nicklaus Wirth
二、复杂度
上面提到,算法的目的是为了求解问题。然而,仅仅是设计出算法是远远不够的。举一个非常简单的例子:
问题 幂运算
x
n
x^n
xn.
输入 正整数
x
,
n
x,n
x,n.
输出
x
x
x的
n
n
n次幂.
非常自然地,我们会想到用一个变量 a n s ans ans 存储结果,初始赋值为1,用简单的循环运算,不停使 a n s = a n s ⋅ x ans=ans\cdot x ans=ans⋅x就行了。代码如下:
ans=1
for i in range(n):
ans*=x
print(ans)
这种算法的复杂度是与输入的 n n n 有关的,而在 n n n 很大时,这种算法会运行得很慢。相比之下,快速幂算法就会比直接模拟快一些:
tmp, ans = x, 1
while n > 0:
if n % 2 == 1:
ans *= tmp
n = n // 2
tmp *= tmp
print(ans)
通过简单的检验发现,这两种算法处理上述问题会给出完全相同的结果,而后者的每次循环会将
n
n
n 减半,这意味着问题的规模增加时,使用快速幂的操作次数增幅更小。
即使算法的效率再高,也不可避免地与问题给出的
n
n
n 产生关联。上述比较带给我们的直观感受是快速幂算法“好”,模拟算法“差”。如何对算法的优劣给出直观定义呢?这就要引出复杂度的概念:
Definition 1.1-1:时间复杂度
仅考虑主要的算法操作,将总操作数与问题规模间的函数关系称作时间复杂度,记作 T ( n ) T(n) T(n) ;所需存储空间与问题规模间的函数关系称作空间复杂度,记作 S ( n ) S(n) S(n) .
多采用时间复杂度对算法进行衡量,复杂度量级越小,处理大规模问题的性能越优。
三、复杂度估计
为了强调算法效率随着问题规模的变化趋势,往往重点关注复杂度的量级。上例中,模拟算法所需的操作与
n
n
n 大致成正比关系,量级应该为
n
(
n
1
)
n(n^1)
n(n1) 。而快速幂算法的每一步将
n
n
n 减半,即双倍的
n
n
n 会使算法多进行一步运算,量级为
log
n
\log n
logn 。(由数学知识,
log
2
n
\log_2 n
log2n 与
ln
n
\ln n
lnn 间仅相差常数因子
ln
2
\ln 2
ln2 ,因此通常忽略底数
2
2
2)
接下来将给“量级”一个严谨的定义,这也是常用的渐近估计技术:
Definition 1-1.2:复杂度上界
对给定的函数 f ( n ) , ∃ C ∈ R , n 0 ∈ N ∗ , ∀ n ≥ n 0 , T ( n ) ≤ C ⋅ f ( n ) f(n) ,\exists C \in \mathbb R,n_0\in \mathbb N^*,\forall n \geq n_0,T(n)\leq C\cdot f(n) f(n),∃C∈R,n0∈N∗,∀n≥n0,T(n)≤C⋅f(n) 恒成立,则称算法具有量级为 f ( n ) f(n) f(n) 的上界,即 T ( n ) = O ( f ( n ) ) T(n)=O(f(n)) T(n)=O(f(n)).
用该上界
O
(
f
(
n
)
)
O(f(n))
O(f(n)) 表示算法复杂度就称作大O表示法。
f
(
n
)
f(n)
f(n)的增长趋势越缓,意味着算法的性能越好。下面将给出使用大O表示法估计复杂度的具体方法。
考虑一个具体问题:
问题 方阵乘积
A
⋅
B
A\cdot B
A⋅B . 方阵
A
,
B
A,B
A,B 的大小均为
n
×
n
n \times n
n×n.
输入 两个方阵
A
,
B
A , B
A,B.
输出 方阵乘积
C
=
A
⋅
B
C=A\cdot B
C=A⋅B.
由高等代数知识,矩阵
C
C
C 的
i
i
i 行
j
j
j 列由下列公式给出:
c
i
j
=
∑
k
=
1
n
a
i
k
⋅
b
k
j
c_{ij}=\sum_{k=1}^{n} a_{ik}\cdot b_{kj}
cij=k=1∑naik⋅bkj
其中的循环变量
i
,
j
,
k
i,j,k
i,j,k 均在 1~
n
n
n 间变化。若设单次运算需要
k
k
k 次基本操作(
k
k
k 为常数),那么这三重嵌套循环总共需要的操作数为
k
⋅
n
3
k \cdot n^3
k⋅n3。由上述分析,该算法的时间复杂度为
O
(
n
3
)
O(n^3)
O(n3)。
同样,回顾最初的幂运算例子,可以得出模拟算法的时间复杂度为
O
(
n
)
O(n)
O(n) ,快速幂算法的时间复杂度为
O
(
log
n
)
O(\log n)
O(logn)。
最后,让我们对估计过程中的一些操作给出一般原则:
- 基本操作:单次四则运算,读,写,赋值操作的时间复杂度为 O ( 1 ) O(1) O(1)。
- 判断语句:if-else 语句的复杂度为两分支的复杂度之和。
- 循环语句:for 循环的复杂度为 单次循环复杂度 × \times ×循环次数。
- 多算法串行:仅计算所有算法中复杂度的最高量级,忽略常数因子。
相信学过OI的都有被 1s,128MB 支配的恐惧,没错,设计更高效的算法总是非常有挑战性的过程。一起为设计更高效的算法而奋斗!