在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。
算法的时间复杂度,也就是算法的时间量度,记作:T(n) = O(f(n))。它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,即T(n)和f(n)是同一个数量级的,称作算法的渐近时间复杂度,简称为时间复杂度。其中f(n)是问题规模n的某个函数。
我们常用“大O记法”来表示时间复杂度,这里的“O”指的是量级(order)。
那如何分析一个算法的时间复杂度?如何推导出大O阶呢?下面给出基本方法:
推导大O阶:
1)用常数1取代运行时间中的所有加法常数。
2)在修改后的运行次数函数中,只保留最高阶项。
3)如果最高阶项存在且不是1,则去除与这个项想乘的常数。
下面用几个具体的例子来理解一下大O推导的过程。
1)O(1)
int sum = 0, n = 100; //执行1次
sum = (1 + n) * n / 2; //执行1次
printf("%d", sum); //执行1次
算法运行次数函数f(n) = 3,根据大O推导的过程,第一步将3改为1;第二步,由于没有最高阶项所以算法的时间复杂度为O(1)。
再举一个例子:
算法运行次数函数f(n) = 12,根据大O推导的过程,第一步将12改为1;第二步,由于没有最高阶项所以算法的时间复杂度还是为O(1)。
由此可以看出该算法的时间复杂度不会随着规模n的变大而变大,即使算法有上千条语句,执行时间也是一个较大的常数项,时间复杂度还是O(1)。
2)O(n)
int i; //执行1次
for(i = 0; i < n; i++) //执行n次
{
printf("%d ", i); //执行n次
}
对于这种带有循环结构的算法,主要还是看循环语句执行的次数。对于该算法,算法运行次数函数f(n) = 2n + 1,根据大O推导的过程,第一步直接过,第二步只保留最高阶项即2n,第三步去除最高阶项的系数2,结果为n,所以时间复杂度为O(n)。
3)O(n^2)
int i, j; //执行1次
for(i = 0; i < n; i++) //执行n次
for(j = 0; j < n; j++) //执行n^2次
{
printf("hello\n"); //执行n^2次
}
对于该算法,算法运行次数函数f(n) = 2n^2 + n + 1,根据大O推导的过程,第一步直接过,第二步只保留最高阶项2n^2,第三步去除最高阶项的系数2,结果为n^2,所以时间复杂度为O(n^2)。
如果将代码改为:
int i, j; //执行1次
for(i = 0; i < n; i++) //执行n次
for(j = 0; j < m; j++) //执行n * m次
{
printf("hello\n"); //执行n * m次
}
那该算法的时间复杂度为O(n * m).
如果将代码改为:
int i, j; //执行1次
for(i = 0; i < n; i++) //执行n次
for(j = i; j < n; j++) //执行n + (n - 1) +......+ 1次
{
printf("hello\n"); //执行n + (n - 1) +......+ 1次
}
对于该算法,算法运行次数函数f(n) = n * (n + 1) + n + 1 = n^2 + 2n + 1,根据大O推导的过程,第一步直接过,第二步只保留最高阶项n^2,第三步过,结果为n^2,所以时间复杂度还是为O(n^2)。
4)O(logn)
int i = 1; //执行1次
while(i < n) { //执行log(2)n次,2为底数,下同
i *= 2; //执行log(2)n次
}
对于log(2)n是怎么求出来的,我们可以假设i *= 2;语句执行了x次,现在每执行一次,i就会乘以2,知道i >= n。所以2^x = n,x = log(2)n。
对于该算法,算法运行次数函数f(n) = 2log(2)n + 1,根据大O推导的过程,第一步直接过,第二步只保留最高阶项2log(2)n,第三步去除最高阶项的系数2,结果为log(2)n,所以时间复杂度为O(log(2)n)。对于底数2,不是确定的,还要看具体的算法,因为如果算法改为i *= 3的话,时间复杂度为O(log(3)n)了。
还有很多种时间复杂度,对于具体算法还是要具体分析,这里只是介绍了如何去分析出算法的时间复杂度,下面附上常见的算法复杂度。