如何分析、统计算法的执行效率和资源消耗?

524 阅读4分钟

「这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战

思考:

常常有人说,我们项目之前都会进行性能测试,再做代码的时间复杂度、空间复杂度分析,是不是多此一举呢?而且,每段代码都分析一下时间复杂度、空间复杂度,是不是很浪费时间呢?你怎么看待这个问题呢?
第一个问题: 性能测试结果很大程度依赖于环境以及测试场景数据量,不同环境不同场景和数据量,结果会明显不同,不好抽象化来评估,而时间空间复杂度分析解耦于环境,可对其做抽象化评估。
第二个问题:
没重点全量分析的确性价比低,个人觉得核心功能核心代码做分析,非核心功能及核心代码可选择忽略不分析。

什么是复杂度分析?

  • 数据结构和算法核心是‘快’和‘省’问题
  • 如何量化考量指标:时间复杂度(快)+空间复杂度(省)
  • 时间复杂度:全称是渐进时间复杂度,表示算法的执行时间与数据规模之间的增长关系。
  • 空间复杂度:全称渐进空间复杂度(asymptotic space complexity),表示算法的存储空间与数据规模之间的增长关系。

为什么需要复杂度分析?

  • 直接运行测试(事后统计法)测试结果非常依赖测试环境与数据规模,故需要一个不用具体的测试数据来测试,就可粗略估算执行效率的方法

如何进行时间复杂度分析

大O复杂度标识法

  • 原理:
    • 代码执行实际与每行代码执行次数成正比,用公式T(n)=O(f(n))表示,T(n)-代码执行的时间、n-表示数据规模、f(n)-每行代码执行次数总和,则公式O-表示代码执行时间T(n)与f(n)成正比。
    • 表示代码执行时间随数据规模增长的变化趋势,公式中低阶、常量、系数并不左右增长趋势,故如T(n)=O(2n2+2n+3)==》T(n)=O(n2)

常见分析技巧

  1. 单段代码看高频:比如 循环
  2. 加法法则:多段取量级最大段
  3. 乘法法则:嵌套取内外乘积

常见复杂度量级

  • 多项式量级
    • 常量阶O(1):算法不存在循环、递归,均属于这类
    • 对数阶/线性对数阶 O(logn)/O(nlogn):归并排序、快速排序
    • 线性阶O(n)
    • 平方阶O(n2)/立方阶O(n3)...k次方阶O(nk)
    • O(m+n)/O(m*n):代码中有两段同量级复杂度,无法事先评估m和n的量级
// 对数阶 O(logn)
i=1; 
while (i <= n){
     i = i * 3; 
}
// O(m+n)
int cal(int m, int n) { 
    int sum_1 = 0; 
    int i = 1; 
    for (; i < m; ++i) {
         sum_1 = sum_1 + i; 
    } 
    int sum_2 = 0; 
    int j = 1; 
    for (; j < n; ++j) { 
        sum_2 = sum_2 + j; 
    }
    return sum_1 + sum_2;
}
  • 非多项式量级:NP(Non-Deterministic Polynomial,非确定多项式),随n增加执行时间会急剧增加,故效益非常低下。比如
    • 指数阶O(2n)
    • 阶乘阶O(n!)

空间复杂度

时间复杂度的全称是渐进时间复杂度,表示算法的执行时间与数据规模之间的增长关系。
空间复杂度全称就是渐进空间复杂度(asymptotic space complexity),表示算法的存储空间与数据规模之间的增长关系。

复杂度4个重要概念

有些情况下,复杂度和测试数据量级或者条件有关,不同条件下,复杂度会有明显区别,这时就需要我们考虑其它维度来表示,故引入这四个概念。

  • 最好情况时间复杂度(best case time complexity)
  • 最坏情况时间复杂度(worst case time complexity)
  • 平均情况时间复杂度(average case time complexity):适用于每个情况下复杂度不一样场景表示,对每个复杂度情况做加权平均值计算,比如,数组中数据查找匹配。
  • 均摊时间复杂度(amortized time complexity):适用于小概率性出现最坏情况复杂度的场景,将小概率最坏复杂度和全部复杂度情况均摊计算,一般情况均摊复杂度等于最好时间复杂度。如下数组的操作示例,触发扩容时复杂度O(n),未触发是O(1),可见一个O(n)伴随n-1个O(1),均摊后为O(1)~
// 全局变量,大小为10的数组array,长度len,下标i。
int array[] = new int[10]; 
int len = 10;
int i = 0;

// 往数组中添加一个元素
void add(int element) {
   if (i >= len) { // 数组空间不够了
     // 重新申请一个2倍大小的数组空间
     int new_array[] = new int[len*2];
     // 把原来array数组中的数据依次copy到new_array
     for (int j = 0; j < len; ++j) {
       new_array[j] = array[j];
     }
     // new_array复制给array,array现在大小就是2倍len了
     array = new_array;
     len = 2 * len;
   }
   // 将element放到下标为i的位置,下标i加一
   array[i] = element;
   ++i;
}