数据结构与算法二:「复杂度分析」

551 阅读5分钟

前前言

今天给大家带来【数据结构与算法】系列文章的第二篇,复杂度分析

复杂度分析对于我来讲太难了,复杂度分析对算法来讲太重要了

什么是复杂度分析

复杂度是针对于算法而讲的,是评价一个算法执行效率的标准。复杂度分为时间复杂度空间复杂度,时间复杂度描述的是程序运行随着数据规模变化时的趋势。 ​

假如要计算算法 A 数据规模为 n,该算法的运行时间为 f(n),随着数据规模 n 的变化,算法执行时间 与 f(n) 成正比,也就是算法执行时间随着数据规模改变时的变化趋势。 所以A 的时间复杂度为 O(n),其中 O 表示变化趋势,在这里表示为正比

类比时间复杂度,空间复杂度描述就是算法所需内存空间随着数据规模变化时的趋势。 与时间复杂度相比空间复杂度会相对简单一些。 ​

复杂点的情况会把时间复杂度拆分为 最好情况时间复杂度、最坏情况时间复杂度、平均情况时间复杂度和均摊时间复杂度。 不管以什么标准去分析算法时间成本,其目的都是为了优化算法,让算法能够提升执行效率,节省空间成本。

大 O 表示法

算法的复杂度通常使用大O表示,在上面已经简单的介绍了一下大O表示法。无论是时间还是空间复杂度,大O所描述的都是数据规模变化导致复杂的增加的趋势,和其他因素无关。现在会有十分成熟的统计,监控软件,能够实时监控程序的运行状态和内存使用状态,并且并不需要我们通过代码进行分析就能够得出正确的值。这种方式计算算法的复杂度会让算法受到其他因素的影响。不能够描述出数据规模影响到复杂度的变化趋势。 使用监控软件统计程序运行时间和空间会受到两种因素影响

  1. 运行环境

    相同的程序在不同的运行环境下,所需的时间是不同的。所以不能够通过监控计算的值进行对比两个算法的好坏。

  2. 数据规模

    监控软件通过测试数据计算运行时间和空间消耗,由于测试数据的规模结构都可能是不同的,所产生的结果也是不同。 ​

所以采用软件软件统计得出的结果会有很多因素的影响,因此需要一种方式,不会受到别的因素影响,通过数据规模就能够得出算法的执行效率,即大O表示法

时间复杂度分析

一般做算法复杂度分析的时候,通常来讲有三种技巧进行分析

只关注循环次数

大O表示法,体现的是随着数据规模变化的一个趋势,所以时间复杂度只和循环的代码段有关。其他的常量部分无关,如下所示

function add(n){
  let sum = 0;
  for (let i = 0;i < n;i++){
    sum+=i
  }
  return sum
}

其中第2行,定义一个sum变量,尽管数据规模 n 如何变化,他运行的次数是一个常量不会变化,而3,4 行会随着数据规模而进行变化,所以我们要分析的就是这段循环的代码。这两段代码被执行了n次,所以时间复杂度是O(n)。需要关注循环次数会根据数据规模改变的代码段

加法法则

时间复杂度的计算的加法法则有两种方式,通常情况下:总的时间复杂度等于量级最大的那段代码

function add(n){
  let sum1 = 0;
  for (let i = 0;i < 10000;i++){
    sum1 += i
  }
  
  let sum2 = 0
  for (let i = 0; i < n; i++) {
    sum2 += i
  }
  
  let sum3 = 0
  for(let i = 0;i < n; i++){
    for(let j = 0; j< n; j ++){
      sum3 += i + j
    }
  }
  
  return sum1 + sum2 + sum3
}

上面代码可以分为三段分析,(1)第3、4 行 循环次数是一个恒定不变的量,无论数据规模n如何边,他还是不变所以这段代码的时间复杂度是O(1);(2)第8、9行,被执行了n次,这段代码的时间复杂度是O(n);(3)第13、15行会被执行 n2n^2 次,这段代码的时间复杂度是O(n2n^2)。那么 add 函数的时间复杂度会取三段代码中运算此时最多的那次即O(n2n^2)。

乘法法则

乘法法则比较简单,即嵌套循环的代码,他的时间复杂度会是嵌套规模的乘积。正如上面代码13、15行所示。 ​

TIPS: 以上分析的都是程序只受一种数据规模影响,如果程序的运行次数会受到两种数据规模的影响,那么加法法则和乘法法则就会有新的形式。请接着往下看。

常见的时间复杂度

如上图所示,复杂度分别是O(n2n^2)、O(nlognnlog n)、 O(nn)、O(lognlog n)、O(1)。结合上图可以得出那种时间复杂度低,实时调整算法的复杂度。他们之间的关系如下 O(n2n^2) > O(nlognnlog n) > O(nn) > O(lognlog n) > O(1) 以上都是在分析单项时间复杂度,如果是多项呢?也就是会受到多种数据规模影响到

O(m+n) O(m*n)

这和上面提到的加法法则和乘法法则有些不同,调整一下上面代码分析一下

function add(n,m){
  let sum1 = 0;
  for (let i = 0;i < 10000;i++){
    sum1 += i
  }
  
  let sum2 = 0
  for (let i = 0; i < n; i++) {
    sum2 += i
  }
  
  let sum3 = 0
  for (let i = 0; i < m; i++) {
    sum3 += i
  }
  
  return sum1 + sum2 + sum3
}

这时候 add 函数会受到两个数据规模m,n影响。第3、4 行不必分析,第8、9行的时间复杂度为O(nn),第13、14行的时间复杂度为O(mm)。那么 add 函数的总时间复杂度是 O(m+nm+n)。也就是说,当程序的复杂度有两个数据规模来决定时,无法评估两个数据规模的大小,所以总时间复杂度为两个数据规模总和。

那么 O(mnm*n),也是有两个数据规模决定,并且两个是嵌套形式。如下:

function add(n,m){
  let sum = 0
  for(let i = 0;i < m; i++){
    for(let j = 0; j< n; j ++){
      sum += i + j
    }
  }
  return sum
}

空间复杂度

空间复杂度描述就是算法所需内存空间随着数据规模变化时的趋势。 通俗来讲,也就是当数据规模增加时变量所需内存空间的数据量会不会随着规模改变。相对于时间复杂度来讲,空间复杂度的分析要简单的多。看两段代码,简单说明一下

function add(n)
  let sum1 = 0;
  for (let i = 0;i < n;i++){
    sum+=i
  }
​
  const arr = new Array(n)
  for(let i = 0;i<n;i++){
    arr[i] = i*i
  }
}

分析以下上面代码的空间复杂度,就是看定义变量的数量与数据规模 n 的关系。 先看第一部分(2~5 行):第 2 行定义一个变量 sum1, 第 3 行定义一个变量 i;除此之外在没有定义别的变量。所以空间复杂度是O(1); 第二部分(7 ~ 10 行):第 7 行定义变量 arr 所需空间大小为 n ,第 8 行 定义变量i所需空间大小为1,所以空间复杂度为O(nn)。总的空间复杂度为O(nn)

总结

复杂度分析为我们提供了一个很好的理论分析的方向,并且它是宿主平台无关的,能够让我们对我们的程序或算法有一个大致的认识。