算法的复杂度

339 阅读2分钟

使用数据结构或者算法解决问题,如何判断这种解法的优劣?

从2个维度去考察:

  • 时间复杂度:用来评估代码运行的时间趋势
  • 空间复杂度:用来评估代码运行的内存占用规模

时间复杂度

下面的代码是求数字1到n的和,那如何评估计算机执行这段代码的时间?

int sum(int n) {
    int sum = 0;
    
    for (int i = 1; i <= n; i++) {
        sum += i;
    }
    
    return sum;
}

假定计算机执行每行代码的时间是单位U。那for循环要执行n次,执行时间就是n倍,那这段代码的执行时间是:

T(n)=(2 + n) * U

时间复杂度不是精确计算代码的执行时间,而是评估随着数据规模的增加,执行时间的增加趋势。

所以可以使用大O表示法,上述公式可以转成T(n)=O(2 + n) 。进一步,当n足够大时,常数可以忽略,变成T(n)=O(n)

经过上文的分析,所有的代码都可以使用大O表示法来评估时间复杂度。常见的时间复杂度有以下几种。

O(1)

O(1)是常量阶级别的时间复杂度,它是指一行或者多行代码,即使包含常数级别的循环或者递归,其时间复杂度也是O(1)。下面的多行代码,其时间复杂度就是O(1)

int a = 1;
int b = 2;
if (b > a) {
  std::cout << "b > a" << std::endl;
}
for (int i = 0; i < 10; i++) {
  std::cout << i << std::endl;
}

O(logn)

O(logn)是对数阶级别的时间复杂度,一般代码中会包含循环,但是循环次数没有那么多。

void test1(int n) {
    int k =  0;
    while (k < n) {
        n = k * 2;
    }
}

上述代码中,根据公式2^x=n,代码循环的次数是以2为底的logn,所以其时间复杂度是O(logn)

O(n)

O(n)是线性阶级别的时间复杂度,一般代码中会包含循环或者递归,并且其数据规模随着变量线性增加。

void test2(int n) {
    int k =  0;
    while (k < n) {
        n = k * 2;
    }
}

O(n^2)

O(n^2)是平方阶级别的时间复杂度,代码中会包含双重循环,并且其数据规模随着变量增加。这个时间复杂度非常高,实际应用中是优化的重点对象。

void test3(int n) {
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            std::cout << i * j << std::endl;
        }
    }
}

空间复杂度

空间复杂度表示代码运行过程中内存占用和数据规模之间的关系。常见的空间复杂度包括O(1)O(n)O(n^2)

比如,下面的代码,最多有numsumi三个变量的内存占用,所以空间复杂度是常量阶O(1)

int sum(int num) {
    int sum = 0;
    
    for (int i = 1; i <= num; i++) {
        sum += i;
    }
    
    return sum;
}

而下面的代码,动态分配的内存空间随着n线性增加,所以空间复杂度是O(n)

void test4(int n) {
    int *p = (int *)malloc(n * sizeof(int));
}

阅读原文,关注[空与一]