文章目录
用时间来度量一个算法的优劣。
实验研究
如果算法已经实现,
在不同的输入下,执行算法并记录每一次执行所花费的时间。
我们可以利用time模块中的time()函数来记录时间。
存在的问题:
- 很难直接比较两个算法的运行时间,需要满足:
a. 实验在相同的硬件和软件环境中执行;
b. 相同的CPU活动情况。 - 实验只有在有限的一组测试输入下才能完成。忽略了输入数据的规模(超多数据);忽略了输入数据的质量(最坏情况)。
- 算法必须完全实现。在算法设计初期,我们并不知道算法的质量。
进一步实验分析
寻找新的评估时间的方法,它需要满足以下要求:
- 在软硬件环境独立的情况下,在某种程度上允许我们评价任意两个算法的相对效率;
- 通过研究不需要实现的高层次算法描述来执行算法;
- 考虑所有可能的输入。
计算原子操作
原子操作有:
- 给对象指定一个标识符;
- 确定与这个标识符相关联的对象;
- 执行算术运算;
- 比较两个数的大小;
- 通过索引访问列表的一个元素;
- 调用函数(不包括函数内的操作执行);
- 从函数返回。
执行时间是常数时间。
度量原子操作数量。
原子操作数与真实运行时间成正比。
最坏情况输入的研究
对于不同的输入,倘若计划用平均运行时间进行分析的话,是具有挑战性的。这要求定义输入的概率分布,从而计算数学期望。
因此,我们分析最坏情况分析。最坏情况分析的设计使得算法更加健壮。
7种常用函数
将算法的时间复杂度与函数联系起来。
我们把原子操作数量描述为输入大小为n的函数
f
(
n
)
f(n)
f(n)。
常数函数
f
(
n
)
=
c
f(n)=c
f(n)=c
描述了在计算机上需要做的基本操作步数。
对数函数
x
=
log
b
n
x = \log_bn
x=logbn
许多算法中的常见操作是反复把一个输入分成两半。
线性函数
f
(
n
)
=
n
f(n)=n
f(n)=n
这个函数出现在我们必须对n各元素做基本操作的算法分析的任何时间。
nlogn函数
f ( n ) = n log n f(n)=n\log n f(n)=nlogn
二次函数
f
(
n
)
=
n
2
f(n)=n^2
f(n)=n2
常出现在嵌套当中。
三次函数和其他多项式
f
(
n
)
=
n
3
f(n)=n^3
f(n)=n3
常出现在嵌套当中。
f
(
n
)
=
a
0
+
.
.
.
+
a
d
n
d
f(n)=a_0+...+a_dn^d
f(n)=a0+...+adnd
常出现在等差数列求和问题当中。
指数函数
f
(
n
)
=
b
n
f(n)=b^n
f(n)=bn
常出现在等比数列求和问题当中。
比较增长率
由大到小依次为:
指数函数,三次函数,二次函数,nlogn,线性函数,对数函数,常数函数。
渐进分析
用数学符号来分析算法。
大O符号(上限)
令
f
(
n
)
f(n)
f(n)和
g
(
n
)
g(n)
g(n)作为正整数映射到正实数的函数。如果存在实数常量
c
>
0
c>0
c>0和整型常量
n
0
≥
1
n_0 \ge 1
n0≥1满足
f
(
n
)
≤
c
g
(
n
)
,
w
h
i
l
e
n
≥
n
0
f(n)\le cg(n),while~n\ge n_0
f(n)≤cg(n),while n≥n0
我们就说
f
(
n
)
i
s
O
(
g
(
n
)
)
f(n)~is~O(g(n))
f(n) is O(g(n))
大O符号蕴含一种小于等于的意思。
大 Ω \Omega Ω符号(下限)
令
f
(
n
)
f(n)
f(n)和
g
(
n
)
g(n)
g(n)作为正整数映射到正实数的函数。如果存在实数常量
c
>
0
c>0
c>0和整型常量
n
0
≥
1
n_0 \ge 1
n0≥1满足
f
(
n
)
≥
c
g
(
n
)
,
w
h
i
l
e
n
≥
n
0
f(n)\ge cg(n),while~n\ge n_0
f(n)≥cg(n),while n≥n0
我们就说
f
(
n
)
i
s
Ω
(
g
(
n
)
)
f(n)~is~\Omega(g(n))
f(n) is Ω(g(n))
大 Θ \Theta Θ符号
令
f
(
n
)
f(n)
f(n)和
g
(
n
)
g(n)
g(n)作为正整数映射到正实数的函数。如果存在实数常量
c
‘
>
0
,
c
′
′
>
0
c‘>0,c''>0
c‘>0,c′′>0和整型常量
n
0
≥
1
n_0 \ge 1
n0≥1满足
c
′
g
(
n
)
≤
f
(
n
)
≤
c
′
′
g
(
n
)
,
w
h
i
l
e
n
≥
n
0
c'g(n) \le f(n)\le c''g(n),while~n\ge n_0
c′g(n)≤f(n)≤c′′g(n),while n≥n0
我们就说
f
(
n
)
i
s
Θ
(
g
(
n
)
)
f(n)~is~\Theta(g(n))
f(n) is Θ(g(n))
比较分析
一、
我们使用大O符号依据渐进增长率来为函数排序。
高增长率也说明了在n很大时,
f
(
n
)
f(n)
f(n)也是巨大的。
二、
我们需要注意被符号隐藏的常数因子和低阶项。
比如:
f
(
n
)
=
1
0
100
n
f(n)=10^{100}n
f(n)=10100n与
f
(
n
)
=
n
2
f(n)=n^2
f(n)=n2,我们更倾向于选择后者。
因为 1 0 100 10^{100} 10100被认为是一个天文数字。
三、
我们一般认为
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)就是高效的;
O
(
n
2
)
O(n^2)
O(n2)在n很小时也被认为是高效的。
区分多项式
O
(
n
c
)
O(n^c)
O(nc)是否高效的关键在于c与1的关系;
区分指数式
O
(
b
n
)
O(b^n)
O(bn)是否高效的关键在于b与1的关系;
算法分析示例
常量时间操作
len(arr) : O(1)
data[I] : O(1)
找最大值算法
对于随机序列,第j个元素比前j个元素大的概率是
1
j
\frac{1}{j}
j1,因此更新最大值的的数学期望为
H
n
=
∑
j
=
1
n
1
j
H_n=\sum_{j=1}^n\frac{1}{j}
Hn=∑j=1nj1(n调和数)。
于是,最大值被更新的预期次数/时间为
O
(
l
o
g
n
)
O(logn)
O(logn)。
前缀平均数
有这样一个特殊的序列,它的元素是另一个序列的前缀平均数。
二次时间算法
O
(
n
2
)
O(n^2)
O(n2)
线性时间算法
O
(
n
)
O(n)
O(n)
初始化列表的时间是 O ( n ) O(n) O(n)
求和项随时更新。
三集不相交
原始方案
O
(
n
3
)
O(n^3)
O(n3)
改进方案
O
(
n
2
)
O(n^2)
O(n2)
最坏情况下,
(a,b)判断,用时
O
(
n
2
)
O(n^2)
O(n2);
(a,c)判断,用时
O
(
n
2
)
O(n^2)
O(n2)。
判别项拆分。
元素唯一性
原始方案
O
(
n
2
)
O(n^2)
O(n2)
改进方案
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
先排序,后操作。
证明技术
- 反证法;
- 数学归纳法;
- 循环不变量。