时间复杂度与算法设计

算法的复杂性分析

  • 算法的效率:时间效率和空间效率

  效率很好理解,每条语句都不是废操作、所有的变量都在起作用那么算法的效率就高。

  一个acmer,时间效率一定要尽可能100%,咱学算法本来就是为了优化时间,没有废操作是优化时间最基本的要求。举个简单的例子:素数筛的内循环是从I*i开始的,i*i之前的数已经在上一次外循环中处理过了,没有必要再处理一遍,这就是减少废操作。

  空间效率是某些算法不可避免的问题,某些算法为了减少构建数据结构、增添新数据、查询数据等操作的时间(或减少代码量),就使用了占用大量内存的数组,数组进行这些操作只需要用下标很方便。典型的例子就是图论里的邻接矩阵和数据结构里的各种树。邻接矩阵是个二维数组,两个下标对应两个点,存的值代表两点之间的权值,而如果这个题目里的图不是复杂图,边不是很多的话,这个二维数组起码有三分之二没有被用到,白白浪费了内存。数组模拟的二叉树结构也是这样,下标为n的结点,左孩子下标为2n,右孩子是2n+1,不管有没有这个结点都要留出这块内存,所以也会有大片内存被浪费。

  尽管有些算法的不可避免的浪费内存,可是我们还是要注意空间效率,因为爆内存也是个棘手的报错。举个例子:有些数组在多组输入的时候被反复使用,就不能把这些数组定义在循环里面,不然每一组数据都要开一个数组,测试数据一多就会爆内存,把定义放在外面或是全局,每一组数据输入之前清空一下数组即可。

  • 空间复杂度:算法在执行中所占储存空间的大小。处理技巧见空间效率。
  • 时间复杂度:将算法中基本操作的执行次数作为算法时间复杂度的度量。如果一个问题的规模是n,解这一问题的某一算法所需要的时间为T(n),它是n的某一函数,T(n)称为这一算法的时间复杂度。但是我们关注的并不是某一次程序运行时间T(n)的值(执行完一段程序的总时间),而是基本操作的总次数。换句话说,我们关注的是T(n)随n的变化趋势,即n趋于无穷大时,算法运行时间的变化趋势。

  总结一个算法时间复杂度的具体步骤如下:

    1)确定算法中基本操作以及问题的规模。(基本操作就是没有重复的操作,最简单的如printf,i++都属于基本操作)

    2)根据基本操作执行情况计算出规模n的函数f(n),并确定时间的时间复杂度为T(n)  = O( f(n)中增长最快项/此项系数 )

  具体来说,就是先观察此算法中输入的数据规模,在竞赛中就是常数级和n级,一般就是n。然后在根据循环和迭代部分的代码求t(n),把t(n)中次数最高的项拿出来,丢掉系数就得到T(n)。准确求解t(n)的方法比较复杂,以程序设计竞赛为主的话也没必要去学,除非遇到很难的题要自创算法。简单求解见下面的代码举例。

  “大O记法”:基本参数n——问题实例的规模;

           把复杂性或运行时间表达为n的函数;

        “O”表示量级,允许使用“=”代替“≈”,如O(2n2+n+1) = O(n2)。

  时间复杂度主要关注点是循环的层数。一层是n,两层是n2,可忽略下一级的所有时间,如O(2n2+n+1) = O(n2),就是只考虑n2的时间,所以此项的系数也可忽略。

如果这个算法的代码执行次数会根据数据有序性而波动,就取最复杂的情况为时间复杂度。时间复杂度考虑的就是最坏的情况。在计算机数学里log2比log10常用的多,所以lg一般指log2。

  常见的时间复杂度有O(1)  O(lgn)O(n)  O(nlgn)  O(n2)  O(nb)  O(bn)  ——从小到大排序,其中b是一个常数,O(1)的意思是不管数据量为多少,程序运行一次的时间始终不变,这是最好的时间复杂度。指数级的时间复杂度很难不超时,我们管这种时间复杂度的问题叫难解性问题。

举例:

void fun1() {
    int x = 0, n = 10000;
    for(int i = 0; i < n; i ++)   //这里单层循环,n是个常数,所以t(n)与n无关,即时间复杂度T(n)恒为O(1)
        x = 0;                    //就一句,t(n) = n的值*1,是个常数。 
}
void fun2() {
    int x = 0, n;
    cin >> n;
    for(int i = 0; i < n; i ++)   //n是输入的,所以数据规模是n 
        x++;                      //假设a条简单操作,t(n) = an。所以T(n) = O(n) 
}
void fun3() {
    int x = 0, n;
    cin >> n;
    for(int i = 0; i < n; i ++)   //n是输入的,所以数据规模是n 
        for(int j = 0; j < n; j++)
            x++;                  //两重循环,循环n*n次,t(n) = n^2, T(n) = O(n^2)
} 
void fun4(int n) {
    int x = 0;
    cin >> n;
    for(int i = 0; i < n; i ++)   //n是输入的,所以数据规模是n
        for(int j = i+1; j < n; j++)
            x++;                  //两重循环,内层循环改动了一下。可算出t(n) = n(n-1)/2, T(n) = O(n^2) 
}
void fun5(int n) {
    int i = 0, s = 0;
    while(s < n) {                // while和for一样看, n的数据规模,但是没执行n次, 
        i++;                      // 算下i和s,可得T(n) = O(√n)
        s += i;
    }
}

//举个复杂的例子,节选于归并的代码
void mergesort(int i, int j) {
    int m;
    if(i != j) {
        m = (i+j)/2;
        mergesort(i, m);          //一次递归 
        mergesort(m+1, j);        //又来一次 
        merge(i, j, m);           //已知此语句复杂度为O(n)
    }
}
由于递归,所以计算变得复杂
f(n) = 2f(n/2)+n //这步肯定没问题,就是下层递归的基本操作函数f(n)*2,加上merge这一句的复杂度。
= 4f(n/4+2n //下层递归
= 2^k*f(n/2^k)+k*n //假设递归深度为k,得到进行k次递归后的f(n)
因为f(1) = 1,
当n = 2^k (k = log2n)时,f(n) = n+nlog2n,所以T(n) = O(nlgn)。
如果看不懂这个计算,就用讲课时画图的直观方法,也能得出nlgn。。。遇到新算法就背下来吧

 既然是简单求解,由于T(n)由t(n)中增长最快的项决定,所以我们没必要算每一个循环和递归的复杂度再加一起,只需要求最复杂的那段代码的复杂度,就可以作为整个算法的时间复杂度了。

 PS:一般来说,如果题目给出限时为1000ms,即1s,代表着最多能进行约1000万次运算,那么如果n是1e5级别的数,选用的算法时间复杂度是n2的话操作次数为1e10,就会超时,而选用的算法nlgn是1e6,符合时间要求。我们在做题中要熟记各算法的时间复杂度,从而放弃一定会超时的写法,能有效避免超时的错误。

 

 

 

 

 

 

 

参考资料: http://www.matrix67.com/blog/archives/105

      https://blog.csdn.net/qq_21768483/article/details/80430590

      算法导论第34章

posted @ 2020-08-03 23:24  jindianli  阅读(434)  评论(0)    收藏  举报