算法与数据结构 时间复杂度
复杂度是衡量一个算法效率高低的一个重要的因素,一般分为时间复杂度和空间复杂度。
空间复杂度,一般在排序等 抽象数据类型的运算和物理实现 有关。本篇主要介绍时间复杂度的一些概念。
我们在 RAM模型:1)内存无限大 2)基本运算O(1) 下面考虑接下来的内容。
算法复杂性 复杂度的概念
“准确的说,算法的复杂性是运行算法所需要的计算机的资源的量。需要的时间资源的量 称为时间复杂性,需要的空间资源的量 称为空间复杂性;这个量应该集中反映算法的效率,从实际的计算机中抽象出来。”
上句源于教材,更加好理解的是,反映 需求资源的量 或者说 复杂性 的东西,就是复杂度,它应该取决于两个因素:(1)问题的规模:n (2)算法的输入:I。
- 复杂性 = 需求资源(空间,时间)的量
- 复杂度 = 反映复杂性的一个概念
复杂度能够反映 当规模n变化的时候,算法花费时间的变化,或者说,程序语句执行次数的变化。
PS:一般来说,需要估计算法的效率的时候,提到复杂性就需要考虑复杂度,两者相互关联,密不可分。
意义:当n处在一个非常大的情况下(n趋近与无穷大 或者 n>=n0)的效率问题。
一:针对每一个元计算进行估计,然后使用公式得到复杂度
如果用C表示复杂性,那么算法的复杂性应该表示为 C(n, I)。根据复杂性分为:时间复杂性和空间复杂性,这个 C(n, I) 可以分为 (1)T(n, I) (2)S(n, I)。
本文主要讨论 T(n, I),在教材的P2: 计算机对每一个元计算 进行统计,然后根据公式来得到 T(n, I)。
缺陷:但是这个 计算机提供的元计算 是非常多的。不可能对规模为n(或者说,规模无穷大)的问题 的每一个合法的输入,都进行一个统计。
那么怎么办嘞?
三个代表来帮忙
二:针对复杂度的三种情况(最好,最坏,平均)来考量算法效率
这里影响算法复杂度的主要因素,是输入。
在上面,由于规模非常大,没有办法对 大规模的问题 的 每一个输入 进行精确的计算。
那么我们选用 三种情况下比较有代表性的复杂度 来作为我们考量算法效率的参考。
以下面的代码为例:
cin >> k; // 1 <= k <= 100
for(int i = 0; i < 10; i++)
{
for(int j = 0; j < k; j++)
{
cout << "Hello" << endl;
}
}
最好情况(也叫做 最优情况):当输入的k为1的时候,输出Hello语句的次数也就 10*1 次,是 所有的输入k中 -> 输出Hello最少 -> 语句执行最少 -> 时间花的最少。
也就是说,由 输入的k 所主导的时间复杂度,在输入k=1的情况下,相对复杂度最低。
最坏情况:当输入的k为100的时候,输出Hello语句的次数达到了 10*100 次。语句执行次数最多,时间花的最多,相对复杂度最高。
平均情况:∑(每种k取值的概率 * 取这种k 情况下的复杂度)。这里的话,每种k取值的概率为 1/100,算出来的平均情况的复杂度为 10*50.5。
以上三种情况下的时间复杂度从不同的角度(最好最坏平均)反映算法的效率,实践证明,可操作性最好且最有价值的是 最坏情况下的时间复杂度。原因:它反映了在问题规模为n的情况下,由输入决定的 问题复杂度的上限。
详细的计算在教材的P2.
但是,还是特别麻烦怎么办?
估计算法复杂度
算法复杂性的渐近性态
定义:T(n)是前面计算算法复杂度的函数,当n趋向于无穷大的时候,T(n)一般也趋向于无穷大。那么对于T(n)来说,如果有一个t(n),在n趋向于无穷大的时候,
(T(n) - t(n))/T(n)
趋向于0,就说t(n)是T(n)的渐近性态。
这个定义又有点让人头疼的意思,大概意思就是说,在n趋向于无穷大的情况下,T(n)和t(n)非常接近,可以近似认为它们相等。
直观上,t(n)是T(n)去除低阶项所留下来的主项。举个例子:T(n) = 3*n^3 + 2*n^2 + n + 3
,那么当n趋向于无穷大的时候,t(n) = 3*n^3
。
因此,当n趋向于无穷大的时候,T(n)渐近于t(n),可以用t(n)来代替T(n),作为复杂性的度量。这种替代是对算法复杂性分析的一种简化。
复杂性的比较 目的:比较两个算法的效率,那么如果比较的两个算法 渐近复杂度的阶数不一样 的时候,只要确定出来各自的阶数,就可以判断阶数小的算法效率高。
此时不用关心包含在t(n)的常数因子,只需要关心最高阶数即可。
引入了四个估计的符号,大O,小o,0中间一横(θ,西塔),Ω,w。
大O的意义是:O(g(n))表示所有以g(n)为上限的函数的集合;当f(n)的复杂度是O(g(n))的时候,在f(n),g(n)不为常数的情况下,它的数学意义是 f(n)属于O(g(n))。
小o也和大O差不多,但是大O的以g(n)为上限,这个上限g(n)是可以取到的;但是小o取不到。
Ω的意义是:Ω(g(n))表示所有以g(n)为下限的函数的集合;w和它差不多,但是取不到下限。
另外一种说法:f(n)的复杂度为O(g(n)) 代表 存在一个常数c,使得当n>n0的时候,
f(n) <= c*g(n)
。
其它的符号可以进行类比。
(1)Θ(西塔):紧确界。相当于"="
(2)O (大欧):上界。相当于"<="
(3)o(小欧):非紧的上界。相当于"<"
(4)Ω(大欧米伽):下界。相当于">="
(5)ω(小欧米伽):非紧的下界。 相当于">"
这五个符号的数学意义,都是集合。
常见问题
(1)类似
O(2*n^3),g(n) = 2*n^3
是否正确?不正确,不应该包含常数。
(2)当评判复杂度的时候,O和Ω不经常出现,因为 f(n) ∈ O(g(n)) 的时候,无非就两种情况:1)f(n) = Θ(g(n)) 2)f(n) = o(g(n))。Ω也是一样的道理。
(3)当O评判的算法复杂度的上限g(n)的阶数越低时,评估越精确;当Ω评判的算法复杂度的下限g(n)的阶数越高时,评估越精确。
(4)O(1) 和 O(2) 的关系:等于关系。由O(1)的定义和O(2)的定义(都是集合,O(1)中的元素以1为上限,O(2)中的元素以2为上限),我们很容易认为 O(1)从属于O(2)。但是,1和2都是常数,而O的数学定义是针对上限为g(n)的函数的。因此我们需要从第二个定义出发,O(1)代表函数f(n)存在一个常数c1,使得n>n0的时候,
f(n) <= c1*1
; O(2)代表函数f(n)存在一个常数c2,使得当n>n0的时候,f(n) <= c2*2
。那么以上两种情况,我们只需要取 c1为2,c2为1 即可让这两个复杂度所代表的意义相同。
因此,O(1)和O(k)(其中k为常数)所代表的意义相同。
常用的复杂度
nlogn,n,n2,n3,logn,1·····
我们一定要选用算法复杂度最低的算法吗?
不一定,有时候算法复杂度最低,性能高,会有一些其他的额外损耗(比如空间换时间),在实际应用中,我们常常选用一种折中的策略,来选用算法。
2016/9/9