一个算法的复杂度可以说也就是一个算法的效率,一般来说分为时间复杂度和空间复杂度。。。
注意接下来说的均是比较YY的,适用与ACM等不需严格分析只需要大致范围的地方,至于严格的算法复杂度分析的那些数学证明,主定理什么的在《算法导论》这本书上有十分详细的讲解,网上应该也会有人写过,这里就不多说了(其实,是我不会而已o(╯□╰)o。。。)。
— 到底啥是复杂度呢?先来个栗子。
小明有10个苹果,有一天他饿了,然后准备吃掉一个苹果,但是小明有中二病,他要吃里面重量最大的那个,于是。。。他需要一个找到那个最大的,可是这应该怎么找呢?
小明要先量一下第一个苹果多重,然后第二个多重,然后量第三个。。。一直到第十个,量的时候记录当前最重的那个,然后当找完了这十个就好了。。。
下面来看看这个神奇的找苹果算法的复杂度,有10个苹果,所以需要测量10次才能找到,如果有100个苹果,显然需要测量100次,1000个呢,1000次,n个也就需要n次。
下面设n为问题的规模,f(n)为运算次数,那么对于小明这个问题来说 f(n)=kn,k是一个常数,这个例子 k=1。(看到如此眼熟的高中数学气息有没有啥感觉。。。)
— 不用多说就知道如果需要的运算次数多的话,需要花费的时间也就多,所以这就是时间复杂度了,对于上面那个问题的时间复杂度就是 f(n) 了。
— 但是。。。对于一个问题的常数 k 是比较难搞定的,可能称一下苹果的重量需要一步操作,也可能两步,也可能好几步。所以对于时间复杂度的分析一般是去找渐进复杂度。简单说就是函数 f(n) 省略了常数和低次项,只留下最高次项。比如函数 6n^3+3n^2+2n+10 变成了 n^3 ,因为当n很大很大的时候,变化趋势就是 n^3 型的,再比如 2^n+n^5 就变成了 2^n,因为指数爆炸嘛。。。
— 这里还要说说符号 O,o 和 Θ,这三个应该高数课会讲。。。O( f(n) ) 表示函数的上界,也就是一个一直比 f(n) 大的函数,然后 o 就是下界,至于 Θ 的话,叫做中界(我YY的一个名字)?也就是和 f(n) 的增长速度一样。。。不过一般来说之后的分析都是用 O 的,因为这个字母好写。。。饿,其实可以理解为因为O是上界,所以算法遇到再坏的情况也不会超过这个函数。
— 对于一个算法来说一般常用的渐进复杂度函数有 O( n ) O( n^2 ) O( n^3 ) O(1) O( 2^n ) O( n!) O( log n) O( n*logn ) O( n*2^n ) 差不多这些,注意 log 指以2为底的对数。。。函数图就像下面这样:
— 然后分别比较看看哪种复杂度更好,显然 O(1)是最好的,因为不管问题的规模有多大,都能一下子得到答案。。。然后看看 O(n),小明的问题就是这个复杂度的,如果问题规模是n,需要运行n次,显然已经不错了,挺快的了。。。但是还有一个更快的,O(log n)的,如果n=1000000 那么才只需要运行 20次不到就能得到答案,但是 O(n)的却需要运行1000000次,你说哪个快。
举个栗子:
要求输入一个n,然后算 1^2+2^2+3^2+4^2+...+n^2的值。
那么先说一种做法,直接for循环,代码如下:
#include <iostream> using namespace std; int main() { int n; int sum=0; cin>>n; for(int i=1;i<=n;++i) sum+=i*i; cout<<sum; return 0; }
显然这种算法的复杂度是 O (n) 的,因为运行了 n 次嘛。(注意 n 比较大的时候结果会超过 int 的表示范围,具体看 ACM录 常识与错误那篇文章有说。)
然后看看第二种做法:推出公式来。1^2+2^2+。。。+n^2 = n*(n+1)*(2n+1)/6
所以第二种做法就是
#include <iostream> using namespace std; int main() { int n; cin>>n; cout<<n*(n+1)*(2*n+1)/6<<endl; return 0; }
这就是 O ( 1 ) 的复杂度了,那么哪个快就不说了。。。
— 至于后面的 O( n^2 ) O( n^3 ) O( 2^n ) 啊啥的,也是这样算,上面那些里面复杂度最高,效率最差的应该是 O(n!),如果 n 是10,就需要运行 3628800 次,这效率,不多说了。。。
— 上面说的是运行的次数,然后说说时间,对于现在的个人计算机来说,速度大约是一秒能运行10000000(7个0)次多,这个可以自己写个程序感受一下,for循环10000000次或者更多看看。。。一般来说如果只有加减法 100000000(8个0)次也很快,但是如果有了除法或者其他东西会慢一点。。。
#include <iostream> using namespace std; int main() { int x; for(int i=0;i<1000000000;++i) x=123*321; // 对比 x=123/321; return 0; }
— 然后对于一个题目来说一般限制了1秒啊2秒啊啥的,那么如果题目的数据规模是100000,那么如果采用的算法是 O(n^2)的,那么就需要运行 10000000000次,也就需要大约1000秒,显然超时了。。。如果算法是 O(n log n)的,那么大约是 1700000 次还是可以接受的。。。至于 O(n) O(1) O(log n)啥的就更不用说了。。。所以说解一个题目的话要注意数据范围和时间限制。。。
— 这里顺便说下常数吧,之前都是把常数忽略了然后看的是一个大致的增长函数。但是如果常数很大,比如算法只有一个 for 循环,但是里面有1000次运算,那么虽然他的复杂度是 O(n)的,但是对于100000的规模需要运行100000000次,也就很可能会超时了。。。所以当常数比较大的时候要注意看看。。。
— 至于空间复杂度的话,也就是用的空间的多少和数据规模n的函数,但是因为这个不是很常用而且和时间复杂度几乎差不多,就不多说了。。。
///////////////////////////////////////////////////
— 那么怎么算复杂度呢,在一些算法和数学的书上有十分严格的数学方法。。。然而,平时不需要的。。。
— 其实靠肉眼看看就差不多知道了,看看有几个循环,大致估算一下运行多少次,然后就知道了。。。这些当写代码前想算法的时候其实就已经大致了解了。。。
— 当然这里对于存在递归的算法,就比较麻烦了,这里建议大家学了一些基本的算法之后再来看,因为这里实在是找不到简单的例子。这里有一个主定理(名字就叫主定理),用来计算递归的函数的复杂度计算。比如说一个算法是把问题分成两个小问题,然后在花费 g(n) 的复杂度来合并两个小问题得到大问题的解。那么函数差不多是 f(n)=2*f(n/2)+g(n) 至于 f(n) 怎么推,就可以用上面的那个主定理(这里可以去网上找找这个定理学习一下),其实。。。也可以先猜一下,然后带入那个等式看看行不行。。。
— 经典的递推比如 f(n)=2f(n/2)+n 的复杂度就是 O(n log n)。。。所以如果 g(n) 如果比 O(n) 要好的话,显然最后的复杂度会比 O(n log n)更好。。。
— 另外还有一些复杂度分析比如均摊分析就更丧心病狂了,这种分析是找平均值,期望值啥的,通过概率或者是其他什么东西证明运行比如100次的平均复杂度一定不会高于一个数,所以平均就是多少多少的,这个的话就不详细多说了,之后学习比较高级的算法的时候才会接触到。。。(其实是我不会。。。)
////////////////////////////////////////////////////
复杂度分析感觉就这些东西,其实大部分算法的复杂度是一眼就能看出来,当然也有一些需要仔细分析这个算法的各种情况才行。。。