• C++
  • C++简单算法复杂度的估算(含多项式、指数复杂度)教程 通俗易懂 详细 0基础

  • @ 2025-3-13 22:04:58

1. 什么是算法复杂度

在编程里,我们常常需要评估一个算法的效率,也就是它解决问题的速度和占用的资源。算法复杂度就是用来衡量算法效率的一个指标,它主要分为时间复杂度和空间复杂度。

  • 时间复杂度:指的是算法执行所需要的计算工作量,它反映了算法执行时间随输入规模增长的变化趋势。
  • 空间复杂度:指的是算法在执行过程中所需要的存储空间,它反映了算法所需存储空间随输入规模增长的变化趋势。

本教程主要关注时间复杂度的估算。

2. 大O表示法

在估算算法复杂度时,我们通常使用大O表示法。大O表示法描述的是算法复杂度的上限,也就是在最坏情况下算法的复杂度。下面是一些常见的大O表示法及其含义:

  • O(1)O(1):常数时间复杂度,算法的执行时间不随输入规模的增加而增加。
  • O(logn)O(log n):对数时间复杂度,算法的执行时间随输入规模的对数增长。
  • O(n)O(n):线性时间复杂度,算法的执行时间随输入规模的增加而线性增加。
  • O(n2)O(n^2):平方时间复杂度,算法的执行时间随输入规模的平方增加。
  • O(2n)O(2^n):指数时间复杂度,算法的执行时间随输入规模的指数增加。

3. 简单算法复杂度估算示例

3.1 常数时间复杂度 O(1)O(1)

如果一个算法的执行时间不随输入规模的增加而增加,那么它的时间复杂度就是 O(1)O(1)

#include <iostream>

// 计算两个整数的和
int add(int a, int b) {
    return a + b;
}

int main() {
    int result = add(3, 5);
    std::cout << "结果: " << result << std::endl;
    return 0;
}

复杂度分析:在 add 函数中,无论输入的 ab 是什么值,函数只执行一次加法运算,执行时间是固定的,不随输入规模的变化而变化,所以时间复杂度为 O(1)O(1)

3.2 线性时间复杂度 O(n)O(n)

如果一个算法的执行时间随输入规模的增加而线性增加,那么它的时间复杂度就是 O(n)O(n)

#include <iostream>

// 计算数组中所有元素的和
int sumArray(int arr[], int n) {
    int sum = 0;
    for (int i = 0; i < n; ++i) {
        sum += arr[i];
    }
    return sum;
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    int n = sizeof(arr) / sizeof(arr[0]);
    int result = sumArray(arr, n);
    std::cout << "数组元素的和: " << result << std::endl;
    return 0;
}

复杂度分析:在 sumArray 函数中,有一个 for 循环,循环的次数等于数组的长度 n。也就是说,随着数组长度的增加,循环执行的次数也会线性增加,所以时间复杂度为 O(n)O(n)

3.3 平方时间复杂度 O(n2)O(n^2)

如果一个算法的执行时间随输入规模的平方增加,那么它的时间复杂度就是 O(n2)O(n^2)

#include <iostream>

// 打印二维数组的所有元素
void print2DArray(int arr[][3], int rows, int cols) {
    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < cols; ++j) {
            std::cout << arr[i][j] << " ";
        }
        std::cout << std::endl;
    }
}

int main() {
    int arr[][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
    int rows = 3;
    int cols = 3;
    print2DArray(arr, rows, cols);
    return 0;
}

复杂度分析:在 print2DArray 函数中,有两层嵌套的 for 循环。外层循环执行 rows 次,内层循环对于外层循环的每一次执行都会执行 cols 次。如果输入规模为 n(假设 rowscols 都近似为 n),那么总的执行次数就是 n×n=n2n \times n = n^2,所以时间复杂度为 O(n2)O(n^2)

3.4 指数时间复杂度 O(2n)O(2^n)

如果一个算法的执行时间随输入规模的指数增加,那么它的时间复杂度就是 O(2n)O(2^n)。一个典型的例子是计算斐波那契数列的递归算法。

#include <iostream>

// 递归计算斐波那契数列的第 n 项
int fibonacci(int n) {
    if (n <= 1) {
        return n;
    }
    return fibonacci(n - 1) + fibonacci(n - 2);
}

int main() {
    int n = 5;
    int result = fibonacci(n);
    std::cout << "斐波那契数列的第 " << n << " 项是: " << result << std::endl;
    return 0;
}

复杂度分析:在 fibonacci 函数中,每次调用都会递归地调用自身两次。随着输入规模 n 的增加,递归调用的次数会呈指数级增长。可以通过递归树来分析,递归树的节点数近似为 2n2^n,所以时间复杂度为 O(2n)O(2^n)

4. 复杂度估算的注意事项

  • 只关注最高阶项:在估算复杂度时,我们只关注最高阶项,忽略低阶项和常数项。例如,O(2n2+3n+5)O(2n^2 + 3n + 5) 可以简化为 O(n2)O(n^2)
  • 最坏情况分析:大O表示法描述的是最坏情况下的复杂度。在实际应用中,算法的实际执行时间可能会比最坏情况要好。

5. 总结

通过以上示例,你应该对算法复杂度的估算有了基本的了解。在编写代码时,我们应该尽量选择复杂度较低的算法,以提高程序的执行效率。同时,要注意不同复杂度的算法在处理大规模输入时的性能差异,例如指数复杂度的算法在输入规模较大时可能会变得非常慢。

1 条评论

  • @ 2025-3-13 22:06:03

    要确定一个算法的时间复杂度属于哪种大O表示法,可以按照下面的步骤来计算和分析。

    1. 理解基本概念

    • 时间复杂度:描述算法执行时间随输入规模增长的变化趋势,用大O表示法表示。大O表示法忽略常数系数和低阶项,只关注影响最大的部分。
    • 输入规模:通常用 n 表示,不同算法中 n 代表的含义不同。例如,在排序数组时,n 是数组的长度;在处理图时,n 可能是顶点数或边数。

    2. 计算步骤

    2.1 找出基本操作

    基本操作是算法中执行时间相对固定的操作,像赋值、比较、算术运算等。算法的执行时间主要由基本操作的执行次数决定。

    例如,下面这段代码:

    int sum = 0;  // 赋值操作
    for (int i = 0; i < n; ++i) {
        sum += i; // 加法和赋值操作
    }
    

    这里的基本操作是 sum += i;i++ 以及 i < n 比较操作。

    2.2 分析操作执行次数

    根据算法的逻辑结构,分析基本操作随输入规模 n 的增长而执行的次数。

    常数时间复杂度 O(1)O(1)

    如果一个算法的基本操作执行次数是固定的,不随输入规模 n 的变化而变化,那么它的时间复杂度就是 O(1)O(1)

    int add(int a, int b) {
        return a + b;
    }
    

    无论 ab 取什么值,add 函数只执行一次加法操作,基本操作执行次数是常数,所以时间复杂度为 O(1)O(1)

    线性时间复杂度 O(n)O(n)

    如果基本操作的执行次数和输入规模 n 成正比,那么时间复杂度就是 O(n)O(n)

    void printArray(int arr[], int n) {
        for (int i = 0; i < n; ++i) {
            std::cout << arr[i] << " ";
        }
    }
    

    for 循环执行 n 次,每次循环执行一次输出操作,基本操作执行次数和 n 成正比,时间复杂度为 O(n)O(n)

    平方时间复杂度 O(n2)O(n^2)

    当算法中有嵌套循环,且每个循环的迭代次数都和 n 相关时,时间复杂度可能是 O(n2)O(n^2)

    void printPairs(int n) {
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                std::cout << "(" << i << ", " << j << ") ";
            }
        }
    }
    

    外层循环执行 n 次,对于外层循环的每次迭代,内层循环也执行 n 次,总的基本操作执行次数是 n×n=n2n\times n = n^2,时间复杂度为 O(n2)O(n^2)

    对数时间复杂度 O(logn)O(\log n)

    当算法每次迭代都将问题规模缩小一定比例时,时间复杂度可能是 O(logn)O(\log n)。典型例子是二分查找。

    int binarySearch(int arr[], int left, int right, int target) {
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (arr[mid] == target) {
                return mid;
            } else if (arr[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return -1;
    }
    

    每次迭代,搜索范围都会缩小一半,基本操作执行次数和 log2n\log_2 n 成正比,时间复杂度为 O(logn)O(\log n)

    指数时间复杂度 O(2n)O(2^n)

    如果算法的基本操作执行次数随输入规模 n 呈指数级增长,那么时间复杂度就是 O(2n)O(2^n)。例如递归计算斐波那契数列。

    int fibonacci(int n) {
        if (n <= 1) {
            return n;
        }
        return fibonacci(n - 1) + fibonacci(n - 2);
    }
    

    每次递归调用会产生两个新的递归调用,递归树的节点数随 n 呈指数级增长,时间复杂度为 O(2n)O(2^n)

    2.3 确定大O表示法

    只保留执行次数表达式中的最高阶项,并忽略其系数。

    例如,某个算法的基本操作执行次数为 3n2+5n+73n^2 + 5n + 7,最高阶项是 3n23n^2,忽略系数 3,时间复杂度就是 O(n2)O(n^2)

    3. 递归算法的时间复杂度分析

    对于递归算法,可以使用递归树或主定理来分析时间复杂度。

    递归树

    递归树是一种直观的方法,通过画出递归调用的树形结构,计算每个层次的节点数和操作次数,最后汇总得到总的操作次数。

    主定理

    主定理适用于形如 T(n)=aT(n/b)+f(n)T(n) = aT(n/b)+f(n) 的递归式,其中 a1a\geq1b>1b > 1f(n)f(n) 是一个渐进正函数。根据 f(n)f(n)nlogban^{\log_b a} 的关系,可以快速确定递归式的时间复杂度。

    4. 最坏、平均和最好情况分析

    • 最坏情况:算法在最不利的输入下的时间复杂度,通常用大O表示法描述。
    • 平均情况:考虑所有可能的输入,计算平均执行时间。平均情况分析通常更复杂,需要用到概率知识。
    • 最好情况:算法在最有利的输入下的时间复杂度。

    一般来说,我们更关注最坏情况的时间复杂度,因为它给出了算法执行时间的上限。

    通过以上步骤和方法,就可以确定一个算法的时间复杂度属于哪种大O表示法。在实际编程中,了解算法的时间复杂度有助于我们选择更高效的算法来解决问题。

    • 1