专栏:数据结构与算法分析
上一篇: (一)数据结构绪论
下一篇:
算法(algorithm) 是对特定问题的求解步骤的一种描述,是指令的有限序列,其中一条指令表示一个或多个操作。
根据算法编制的程序最终是要运行在计算机上,由计算机执行程序最终得出结果。所以我们需要考虑的是程序执行完毕得出结果需要多少时间,以及占用了多少资源(通常指占用内存)。
现在个人计算机的内存容量多数已经达到了4GB 和 8GB,而专门用于实验室的计算机,内存甚至已经达到了256GB以上。目前多数情况下,问题的规模不超过千万,足以存储,但是目前计算机的运行速度仍相对有限,特别当规模较大时,在执行时间上,不同效率的算法天差地别,有些算法只需要运行几秒就能得出答案,而另一种算法可能需要数千年。
目前来说,更为重视算法的运行时间,而不是内存空间占用大小。因此大规模问题的内存占用,现在的计算机基本上可以满足要求,并且内存并不是一次性消耗品,程序执行结束后,内存即可得到释放,并没有产生消耗。但是算法的运行时间则不同,不同效率的算法之间的差别过大,导致如果算法设计得不够好,即使小规模的问题,计算机仍需要数小时、数天来完成计算,当规模变得更大后,计算机在报废前完成计算都变成了奢望。
当然,也不是说内存空间占用就完全不需要理会,而是说目前的存储器容量在大规模问题上都可以满足大多数算法的需求,大多时候存在空闲空间,很少会出现需要远超于问题规模的存储空间的情况。反而是一般效率的算法,处理中等规模问题时很有可能做不到数秒内完成,所以一般是优先考虑运行时间,甚至为了更快还会以空间换时间。
估计算法资源消耗所需,通常有两种方法,分别是实验统计和理论分析。
实验统计 的方法是直接按照对应算法编写程序,然后分别记录不同规模的输入时程序所需的运行时间和占用空间。
这种方式虽然可以得到较为准确的运行时间和占用空间数据,但是需要由算法编写出相应的程序并在机器上运行,比较费时间。对于同一个算法,使用不同的编程语言编写或者同一个编程语言使用不同的实现也会对运行时间造成影响。同时,计算机硬件配置不同,运行速度有快有慢,得到的数据也只能作为参考,并不说明在其它机器上也消耗这么多资源。
对实验统计得到的数据进行拟合,可以得到算法所需资源消耗与问题规模的关系。
理论分析方法则是直接根据算法或相关代码来得到运行时间与问题规模大小的关系。
理论分析的方法仅仅是得到资源消耗与问题规模大小之间的关系,并非是实际时间数据,因此,要估算出算法所需的资源消耗,还需要在计算机上运行实际程序,得到输入在某个规模大小时对应时间值和占用空间大小,以此推算出算法在不同问题规模下所需的相应资源消耗。
理论分析方法得到的资源消耗与问题规模大小之间的关系,在不同的设备上,不同的环境都通用,可以借此来对算法的好坏做一个评估。
实验统计和理论分析方法各有优势。理论分析较为简便,通用性好,但无法得到实际数据,只能估算。实验统计方法耗时久,受环境因素影响大,但针对性强。
当程序实际运行环境确定后,实验统计方法得到的数据更接近实际,并且可以用来验证理论分析所得出结果。所以通常采用理论结合实际的方式,在理论分析完毕后进行实验统计,不仅可以验证算法的正确性,还能防止意料情况的发生(如问题达到一定规模后,占用内存超出设备内存容量等)。
算法复杂度 即算法所需资源与数据规模之间的增长关系,分为 时间复杂度 和 空间复杂度。
算法总是假定运行在计算机上,但是并不指定计算的性能。为了便于分析,我们把计算机执行一条简单指令或执行由数条简单指令构成的复合指令的时间都定为1。
相当于如果两条语句分别是执行一次加法和执行3次乘法,它们的运行时间认为是相同的,这么做是为了简化分析。分析时先得出简单的关系,如果有必要,再进行细致的分析。
下面我们尝试对一个实现的简单算法分析其运行时间与数据规模大小之间的关系。
这里是一个计算通项公式为 a i = i 3 a_i = i^{3} ai=i3 的数列的前 N N N项和 S ( N ) = 1 3 + 2 3 + 3 3 + ⋯ + N 3 S(N) = 1^{3} + 2^{3} + 3^{3} + \cdots + N^{3} S(N)=13+23+33+⋯+N3 的简单程序片段:
int partialSum(int N)
{
int sum = 0; //1. 初始化,执行1次, 运行时间算作 1
for (int i = 1; i <= N; i++) //2. 循环,执行N+1次,运行时间算作 N+1
sum += i * i * i; //3. 执行N次, 运行时间算作 N
return sum; //4. 执行1次, 运行时间算作 1
}
函数中的代码一共4句,由于我们将语句的运行时间计算简化,所以对于有固定数量指令组成的顺序语句(第1句和第4句),运行时间都算作1,而循环语句我们则根据其执行的次数计算。
- 第1句执行 1 次,运行时间算作 1
- 第2句循环执行N+1次(i从1开始,等于N+1时不符合循环条件退出,共N+1次),运行时间算作 N+1
- 第3句循环执行N次( i 从1开始,共计算到N),运行时间算作 N
- 第4句执行 1 次,运行时间算作 1
按照以上的分析,运行时间
T
(
N
)
=
1
+
(
N
+
1
)
+
N
+
1
=
2
N
+
3
T(N) = 1 + (N+1) + N + 1 = 2N+3
T(N)=1+(N+1)+N+1=2N+3。
最后可以得出,上面算法的运行时间
T
(
N
)
T(N)
T(N)与
N
N
N的关系为:
T
(
N
)
=
a
N
+
b
T(N)=aN+b
T(N)=aN+b 于是我们得到,运行时间和数据规模大小呈线性关系。那么到底正不正确呢?我们通过实验统计的方法,来实际运行上面的函数,记录运行时间,得到以下数据:
规模大小 N N N | 运行时间 T ( N ) T(N) T(N) (单位:ms) |
---|---|
1亿 | 26 |
2亿 | 56 |
4亿 | 114 |
8亿 | 216 |
通过由数据绘制成的散点图可以看到,这些点布大致分布在一条直线上,和之前的结论一致,并且由线性关系可以得到数据规模变为原来的两倍,则运行时间也变为原来的两倍。
由此我们得出,当数据规模大到一定程度时,
T
(
N
)
=
a
N
+
b
T(N)=aN+b
T(N)=aN+b 中的常数项
b
b
b 对整体运行时间的影响可以忽略不计,此时我们可以将关系
T
(
N
)
T(N)
T(N) 简化为
T
(
N
)
=
a
N
T(N) = aN
T(N)=aN。并且,我们在粗略估计时,实际上只关心数据规模增长到原来的
k
k
k倍时,运行时间大约变为原来的多少倍,这时系数
a
a
a对这个倍数的并无影响,运行时间
T
(
N
)
T(N)
T(N) 与 数据规模
N
N
N成正比。
如果随着数据规模
N
N
N的增大,算法的运行时间
T
(
N
)
T(N)
T(N) 的增长与数据规模
N
N
N 成正比关系,那么我们称算法的时间复杂度为线性阶,记作
T
(
N
)
=
O
(
N
)
T(N) = O(N)
T(N)=O(N) 如果运行时间
T
(
N
)
T(N)
T(N) 的增长与数据规模
N
N
N 的平方
N
2
N^2
N2成正比关系,则称算法的时间复杂度为平方阶,记作
T
(
N
)
=
O
(
N
2
)
T(N) = O(N^2)
T(N)=O(N2) 以此类推,算法的时间复杂度还有立方阶
O
(
N
3
)
O(N^3)
O(N3),对数阶
O
(
log
N
)
O(\log N)
O(logN) ,线性对数阶
O
(
N
log
N
)
O(N \log N)
O(NlogN), 指数阶
O
(
2
N
)
O(2^N)
O(2N) ,常数阶
O
(
1
)
O(1)
O(1) 等。
我们试着画出其中的几个复杂度,分别是 O ( N ) , O ( log N O(N), O(\log N O(N),O(logN可以看到,在 N N N 值较小时,各复杂度差别不大。
注意,计算机中 log N \log N logN没有明确说明底数时, 一般指的是以 2 2 2 为底数的对数 log 2 N \log_2 N log2N,因为 log a N = log 2 N log 2 a \log_a N=\dfrac{\log_2 N}{\log_2 a} logaN=log2alog2N,仅仅相差一个常数系数 1 log 2 a \dfrac{1}{\log_2 a} log2a1,而讨论复杂度时,一般会忽略掉系数,所以也不会出现 O ( N 2 ) O(\frac{N}{2}) O(2N)之类的写法。
随着规模的增大,不同复杂度之间的差距越来越大,规模仅仅到100,指数阶 O ( 2 N ) O(2^N) O(2N) 已遥遥领先,其它复杂度所引起的时间增长几乎可以忽略不计,这就是为什么我们之前在对 T ( N ) = a N + b T(N) = aN+b T(N)=aN+b做粗略估计时,只看最高的线性阶,而不管常数 b b b 的值,因为当规模达到一定大小时,低阶项所带来的影响微乎其微。
我们可以进行一个简单的计算,当数据规模增长为原来的1000倍时,复杂度为线性阶 O ( N ) O(N) O(N) 的算法运行时间增长为原来的1000倍,而平方阶 O ( N 2 ) O(N^2) O(N2)则增长为原来的一百万倍,指数阶 O ( 2 N ) O(2^N) O(2N) 的增长已经超过 1 0 300 10^{300} 10300倍,对数阶 O ( log N ) O(\log N) O(logN)仅仅为原来的 10 10 10 倍,常数阶 O ( 1 ) O(1) O(1) 则不变。
空间复杂度 主要关心随着问题规模的增大,占用内存空间的增长率,记作 S ( N ) S(N) S(N)。
空间复杂度只计算额外需要的内存空间,原本数据元素所占的内存空间大小不计算在内。
和时间复杂度类似,同样有线性阶 O ( N ) O(N) O(N),平方阶 O ( N 2 ) O(N^2) O(N2) ,对数阶 O ( log N ) O(\log N) O(logN) 和常数阶 O ( 1 ) O(1) O(1) 等,最常见的是常数阶和线性阶。
专栏:数据结构与算法分析
上一篇: (一)数据结构绪论
下一篇: