时间复杂度计算方法以及常见的时间复杂度

时间复杂度衡量着一个程序的好坏,时间复杂度的估算是算法题的重中之重。但是很多初学者对于时间复杂度缺少一种概念,对于复杂程序的估算难以理解,理解不了时间复杂度,算法学习无从下手 。因此为了解决对时间复杂度的理解难题,本文将从简单到复杂介绍时间复杂度的计算方法,以及常见的时间复杂度,足以应付百分之八十的算法题。

一、时间复杂度的概念理解


一般来说要确定算法的运行时间,只有你将他拿到机器上去测试一下才能确定,但是过于麻烦,那么有没有一种方法能够估算该算法的运行时间呢?因此引入了时间复杂度这个概念。

时间复杂度是一种函数,定量地描述了该算法运行的时间。既然是一种函数,就涉及到自变量与因变量。因变量代表是时间复杂的规模,自变量是时间复杂度的执行时间。这里的执行时间并不是秒,分钟这类的具体时间,它表示的是一种“执行次数”。要想计算时间复杂度首先得找到该算法中的循环,算法中循环执行的次数就是算法的时间复杂度

算法的时间复杂度的具体表示为:用大写的O来体现算法时间复杂度如O(f(n)),称之为大O记数法

二、时间复杂度的计算


  1. 给定 n个元素 的数组a[n],求其中 奇数 有多少个:
    判断一个数是偶数还是奇数,只需要求它除上 2 的余数是 0 还是 1,把所有数都判断一遍,并且对符合条件的情况进行计数,最后返回这个计数器就是答案,需要遍历所有的数,因此代码为:
    int count(int n, int a[]) {
      int cnt = 0;
      for(int i = 0; i < n; ++i) {
        if(a[i] % 2)
          ++cnt;
      }
      return cnt;
    }
    
    由代码段知,该函数中只有一层for循环,而该循环执行了n次,因此时间复杂度为O(N);
  2. 求下面函数的时间复杂度:
    int fun(int n) {
      int cnt = 0;
      for(int i = 0;i < n;i++) {
        for(int j = 0; j<n; j++) {
          cnt++; 
        }
      }//两层循环,每次循环n次,因此为n*n
     
      for(int k = 0; k<n; k++) {
        ++cnt;
      }//一层循环,循环n次
      
      for(int l = 0;l<10;l++) {
        ++cnt;
      }//一层循环,循环10次
    
      return cnt;
    }
    
    由注释,可列出计算时间的复杂度的表达式:n*n+n+10。但是我们能写成O(N*N+N+10)吗?我们知道,对于时间复杂度我们不需算出精确的数字,只需要算出这个算法属于什么量级即可,我们又如何知道它属于哪个量级呢?即,我们将字母取无穷大,例如本题中字母为n,n取无穷大,而十对于n取无穷大后没有影响,因此10可以舍去,原表达式化为n*n+n,再简化为n*(n-1),由于n为无穷大,因此-1也是没有影响的,原式就变成了O(N*N)。这就是大O渐近表示法,只是一种量级的估算,而不是准确的值

由此可以得出计算时间复杂度的一般规律(用大O表示法):

  • 去除表达式中所有加法常数;
  • 修改的表达式中只保留最高阶项,因为只有它对最终结果产生影响;
  • 如果最高阶项系数存在且不是1,则将其系数变为1,得出最后的表达式。
  1. 计算冒泡排序的时间复杂度:

    void bubblesort(int* a,int n) {
      assert(a);
      for(int end = n; end>0; end--) {
        int exchange = 0;
        for(int i = 1; i<end; i++) {
          if(a[i-1]>a[i]) {
            swap(&a[i],&a[i-1]);
            exchange = 1;
          }
        }
        
        if(exchange==0)
          break;
      }
    }
    

    例如在这个冒泡排序中,我们需要将无序数组转化为有序数组的一种算法,它并不像上题一样是简单的双层嵌套循环,很容易想到它的循环次数是一个等差数列,第一次循环n-1次,第二次n-2次.....一直到1.因此为n-1+n-2+n-3.....+1 = n*(n-1)/2,由上面所说的规律时间复杂度为O(N*N)。

    通过上面的例子我们看出,大O渐近表示法去掉了对结果影响不大的项,简洁明了地表示出了时间复杂度。在实际情况中一般只关注算法的最坏运行情况

    • 例如在上述冒泡排序中,如果给定的数组就已经是有序的了,那么就是它的最好情况,时间复杂度为O(N)。但是如果有非常多的数据很显然我们看不出它到底是否为最好情况,所以我们必须用最坏的期望来计算所以它是O(N*N)。
  2. 时间复杂度O(1):

    int fun(int n) {
      int i = 0;int cnt = 0;
       for( i; i<100;i++)
         {
           cnt++;
          }
      return cnt;
    }
    

    此时时间复杂度为O(1),这里的1不是指一次,而是常数次,该循环执行了100次,不管n多大,他都执行100次,所以是O(1)。

三、常见的时间复杂度


  1. 常数阶
    函数内循环为常数次或者没有循环,例如上面第4题,时间复杂度为O(1)。
  2. 线性阶
    就像上面第一题一样,只有一层循环,时间复杂度随n的增大线性增加,函数在图像上表示为一条经过原点的直线,时间复杂度O(N)。
  3. 对数阶
    例题:给定 n 个元素的升序有序数组 a[n] 和整数k,求 k 在数组中的下标,不存在输出 -1。
    这道题是经典的查找问题,一般最快的情况是使用二分查找:
    int binary(int n, int a[], int k) {
      int left = 0, right = n - 1;
      while(left <= right) {
        mid = (left + right)/2;
        if(a[mid] == k) 
          return mid;
        else if(a[mid] < k)
          right = mid + 1;
        else
          left = mid + 1;
      }
      return -1;
    }
    
    left和right指数组最左边和最右边的下标。每次将这个数组砍一半,求出mid中间下标。由于是升序排列,如果中间下标代表的数大于给定的数k,那么k必定在中间下标的左边。那么就将mid+1的值赋给right,反之则将mid+1的值赋给left,每次将数组砍一半直到找到数k为止。
    n->n/2->n/4->n/8.....直到n变为1
    这个循环次数是对数的值,很显然是以二为底,数组个数n的对数O(log2n)。
  4. 指数阶
    指数阶一般是算法题的暴力解法,一般是多层循环的嵌套,例如上面题二中,最大是两层n次循环的嵌套,因此时间复杂度为O(N2),要是三层n次循环的嵌套则为O(N3)。
  5. 根号阶
    例题:给定一个数 n,问 n 是否是一个素数。
    常见的方法就是暴力法,n和每个小于n大于1的数相除如果都除不进,则n为素数。不过这次我们选择更为简单的方法例如:
    bool isPrime(int n) {
      int i;
      if(n == 1) {
        return false;
      }
    
      int sqrtn = sqrt(n);
      for(int i = 2; i <= sqrtn; ++i) {
        if(n % i == 0) {
          return false;
        }
      }
      return true;
    }
    
    只需要枚举所有小于根号n的数,使n与其相除,这样时间复杂度就小了很多。那为什么只需要枚举小于根号n个数呢?
    因为假设n是数k的因子,那么n2也必定是数k的因子,所以不需要枚举小于k这么多数,只需要枚举根号n个数就可以了。
  6. 阶乘阶
    阶乘阶的讨论没有意义,阶乘级的时间复杂度一般在刷题时过不了,一般会用动态规划代替。

版权声明:本文为CSDN博主「阿亥」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:时间复杂度计算方法以及常见的时间复杂度

posted @ 2022-12-29 13:47  Fogram  阅读(750)  评论(0编辑  收藏  举报