在计算机科学中,算法的时间复杂度(Time complexity)是一个函数,它定性描述该算法的运行时间。时间复杂度常用大O符号表示,不包含这个函数的低阶项和最高项系数。使用这种方式时,时间复杂度称为渐进时间复杂度,亦即考察输入值大小趋近无穷时的情况。
为了计算时间复杂度,我们通常会估计算法的操作单元数量,每个单元运行时间都是相同的。因此,总运行时间和算法的操作单元数量最多相差一个常数系数。
“算法”与“时间”
首先看“算法”,算法是解决特定问题的方法,经过编译器编译后,变成了指令序列。
故,“算法”是指令序列。
再看“时间”,此处的时间指执行算法消耗的时间,也就是计算机执行一系列指令的时间。
故,“时间”是执行算法所包含的指令消耗的时间。
这个时间受:
- 计算机执行每条指令的速度 -> 硬件层面
- 编译产生的代码质量 -> 软件层面
- 算法的好坏(算法使用的策略)
- 问题的规模
事后评估法
比较两个算法的优劣,最简单的方法就是将两个算法都跑一遍,通过统计、监控,就能得到算法的执行时间。为什么还要做时间复杂度分析?
这种做法是正确的,但存在局限性:
- 测试结果非常依赖测试环境。计算机执行每条指令的速度,编译器的不同都有可能导致算法执行时间的不同,进而掩盖算法本身的特性。
- 测试结果受数据规模影响大。
总结来说,这种方法依赖于具体的测试数据测试,这种测试方法是依赖于底层硬件和软件的,不能真实反应算法的性能。因此这种直接用日常的时间来度量算法是不可靠的。
时间复杂度
我们希望有一种方法,能够不依赖于具体的数据和具体的环境就可以比较算法的优劣。这个方法就是时间复杂度分析。
除了使用日常的时间度量算法的优劣,还可以用算法执行所需要的步骤总数来度量。显然,这个步骤总数与具体的环境是无关的,只和算法本身和数据的规模有关。
于是我们就用算法执行所需要的步骤总数作为算法优劣的度量标准。
我们把算法程序中每一步看作是一个基本的计量单位,那么一个算法的执行时间就可以看作是解决一个问题所需要的总步骤数。
这里将具体的时间抛弃,使用算法程序的步骤总数作为度量标准。使得我们的分析,不再依赖于具体的硬件环境和软件环境,这样的比较更能反应算法本身的性能。
// 求n的阶乘
int f(n)
{ // cost times
int sum = 0; // c1 1
int temp = 1; // c2 1
for(int i = 1; i <= n; i++){ // c3 n
temp = i * temp; // c4 n
sum += temp; // c5 n
}
}
/*
cost是每行代码执行的时间,下标代表时间不同,times代表执行的次数
这个算法的执行时间 = (c3+c4+c5) * n + c1 + c2 = c * n + c',其中c3+c4+c5 = c, c1+c2 = c'
这样我们就得到了算法执行时间与输入规模n的关系。在这里是线性关系。
从这里可以看出,我们算法的执行时间与输入规模n成线性增长。
我们知道,这里的c1...c5我们是无法确定的。因此最后的执行时间也是不确定的。
因此,其实我们的时间复杂度分析,分析的是执行时间随输入规模n的一个增长的趋势或者说是量级,
它是无法分析出具体的时间的,就算分析出来也是依赖于具体环境的。
这个增长趋势是与具体环境无关的,除此之外这个趋势就已经可以让我们比较出算法的优劣了。不管c1...c5是什么,它总归是一个常数。对执行时间的增长速度不起决定性作用。
例如 c1*n+c2, c3*n^2+c4。我们很容易知道,随着n逐渐增大,最终c2*n^2+c1会远远大于c1*n,而不管c1...c4是多少。
如果我们忽略掉每行代码的真实代价,即将c3+c4+c5的和统一看成c,将c1+c2看成c',
那么我们在分析时间复杂度时,就只需要分析代码执行的次数,无须关心不同代码的真实代价。
因为它们不影响我们得到正确的增长趋势。
大O表示法则是最简表示。
*/
大O表示法
大O时间复杂度并不具体表示代码真正的执行时间(上面也说了这个时间是无法确定的),而是表示代码执行时间随数据规模增长的变化趋势,所以,也叫作渐进时间复杂度。因此上面的代码的时间复杂度用大O表示法就是T(n)=O(n)。忽略所有常数和低阶项和最高项系数,保留最高项。这样我们就能知道算法的执行时间与输入规模的增长量级。然后利用这个量级进行比较就行了。
时间复杂度的分析
1. 只关注循环执行最多的一段代码。
因为大O表示法其实只是表示一种增长趋势,因此我们通常会忽略掉式子中的常数,低阶项、系数,只需要记录一个最大阶的量级就行了。所以我们在分析一个算法、一段代码的时间复杂度时,只需要关注循环执行次数最多的那一段代码就行了。