判断算法A和 B哪个更好
假设2个算法的输入规模都是n
算法A要做2n+3次操作
算法B要做3n+1次操作
次数 | 算法A(2n+3) | 算法A’(2n) | 算法B(3n+1) | 算法B’(3n) |
n=1 | 5 | 2 | 4 | 3 |
n=2 | 7 | 4 | 7 | 6 |
n=3 | 9 | 6 | 10 | 9 |
n=10 | 23 | 20 | 31 | 30 |
n=100 | 203 | 200 | 301 | 300 |
分析:
当n=1时,算法A的效率不如算法B。而当n=2时,两者效率相同;当n>2时,算法A就开始优于算法B了。
随着n的增长,算法A比算法B越来越好(执行的次数比B要少)。
结论:算法A总体上好过算法B。
定义:输入规模n在没有限制的情况下,只要超过一个数值N,这个函数就总是大于另外一个函数,我们称函数是渐进增长的。
函数的渐近增长
给定两个函数f(n)和g(n),如果存在一个整数N,使得对于所有的n>N,f(n)总是比g(n)大,那么,我们说f(n)的增长渐近快于g(n)。
深入分析
1. 我们可以忽略加法常数。
2. 与最高次项相乘的常数并不重要。
3. 最高次项指数大的,函数随着n的增长,结果也会变得增长特别快。
结论:判断一个算法效率时,函数中的常数和其他次要项常常可以忽略,而更应该关注主项(最高阶项)的阶数。
某个算法,随着n的增大,它会越来越优于另外一算法,或者差于另一算法。
算法时间复杂度
在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况确定T(n)数量级。算法的时间复杂度,也就是算法的时间度量,记住:T(n)=O(f(n))。
它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称为时间复杂度。其中f(n)是问题规模n的某个函数。
随着n的增长,T(n)增长最慢的算法最优。
前面的3个求和算法的时间复杂度分别为O(n),O(1),O(n^2),它们的非官方名称,O(1)叫常数阶,O(n)叫线性阶,O(n^2)叫平方阶。
算法导论中关于O的解释
•O(g(n))={ f(n): 存在正常数c和n0,使对所有n>=n0,有0<=f(n)<=cg(n) }
推导大O阶方法
1. 用常数1取代运行时间中所有加法常数。
2. 在修改后的运行次数函数中,只保留最高阶项。
3. 如果最高阶项存在且不是1,则去除与这个项常数项相乘的常数。
得到的结果就是大O阶。
常数阶
Int32 sum = 0, n = 100;//执行了1次
sum = (1 + n) * n / 2;//执行了1次
Console.WriteLine("sum={0}", sum);//执行了1次
这个算法的运行次数是f(n)=3,根据推倒大O阶的方法,第一步就是把常数阶项3改为1。
所以这个算法的时间复杂度为O(1)。
另外一个例子:
Int32 sum = 0, n = 100;//执行了1次
sum = (1 + n) * n / 2;//执行了1次
sum = (1 + n) * n / 2;//执行了2次
sum = (1 + n) * n / 2;//执行了3次
sum = (1 + n) * n / 2;//执行了4次
sum = (1 + n) * n / 2;//执行了5次
sum = (1 + n) * n / 2;//执行了6次
sum = (1 + n) * n / 2;//执行了7次
sum = (1 + n) * n / 2;//执行了8次
sum = (1 + n) * n / 2;//执行了9次
sum = (1 + n) * n / 2;//执行了10次
Console.WriteLine("sum={0}", sum);//执行了1次
这个算法的时间复杂度还是O(1),这种与问题大小无关(n的多少),执行时间恒定的算法,我们称为具有O(1)的时间复杂度,又叫常数阶。
线性阶
分析算法的复杂度,关键就是要分析循环结构的运行情况。
for (Int32 i = 0; i < n; i++)
{
//时间复杂度为O(1)的程序步骤序列
}
上面代码的时间复杂度为O(n)。
对数阶
Int32 count = 1, n = 100;
while (count < n)
{
count = count * 2;
//时间复杂度为O(1)的程序步骤序列
}
每次count乘以2以后,就距离n更近了一分,也就是说2要乘以多少个2后大于n,则退出循环,由2^x=n得到x=log2n。所以这个循环的时间复杂度为O(logn)。
平方阶
for (Int32 i = 0; i < n; i++)
{
for (Int32 j = 0; j < n; j++)
{
//时间复杂度为O(1)的程序步骤序列
}
}
这个算法的时间复杂度为O(n^2)。
如果把外层循环的次数改成了m,时间复杂度就变为O(m*n);
总结:循环的时间复杂度等于循环体的复杂度乘以该循环运行的次数。
对于下面的循环
for (Int32 i = 0; i < n; i++)
{
for (Int32 j = i; j < n; j++)
{
//时间复杂度为O(1)的程序步骤序列
}
}
当i=0时,内部循环执行了n次,当i=1时,执行了n-1次,…当1=n-1时,执行了1次。
所以总的执行次数为
N+(n-1)+(n-2)+…+1=
用推导大O的方法,保留最高项,以此留下n^2/2;去除这个想的常数项,最终这个算法的时间复杂度为O(n^2);
方法调用的时间复杂度
for (Int32 i = 0; i < n; i++)
{
Test(i);
}
static void Test(Int32 i)
{
Console.WriteLine("i={0}", i); //时间复杂度为O(1)的程序步骤序列
}
这个代码的时间复杂度为O(n);
常见的时间复杂度
执行次数函数 | 阶 | 非正式术语 |
12 | O(1) | 常数阶 |
2n+3 | O(n) | 线性阶 |
3n^2+2n+1 | O(n^2) | 平方阶 |
5log2n+20 | O(logn) | 对数阶 |
2n+3nlog2n+19 | O(nlogn) | nlogn阶 |
6n^3+2n^2+3n+4 | O(n^3) | 立方阶 |
2^n | O(2^n) | 指数阶 |
常用的时间复杂度从小到大排序:
O(1)<O(logn)<O(n)<O(nlogn)<O(n^2)<O(n^3)<O(2^n)<O(n!)<O(n^n)
最坏情况与平均情况
最坏情况运行时间是一种保证,那就是运行时间将不会再坏了。在应用中,这是一个最重要的需求,通常,除非特别指定,我们提到的运行时间都是最坏情况的运行时间。
平均运行时间是所有情况中最有意义的,因为它是期望的运行时间。