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

在这里插入图片描述


@[toc]

算法复杂度

算法的执行效率,粗略地讲,就是算法代码执行的时间。但是,如何在不运行代码的情况下,用“肉眼”得到一段代码的执行时间呢?

我们来看一个栗子:

写一段累加的代码,很多书里面都是用的这个栗子吧:

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

可以看出,这段代码总共要执行n次(因为循环)。
所以我们把时间复杂度记为O(n),这种表示法称为 “大O表示法”。

为什么记为O(n)呢,记每行代码执行的时间为 unit_time ,这段代码总的执行时间就是 (2n+2)*unit_time,因为这段代码的执行时间与每行代码执行的次数成一次正比。

再看一段代码:

int n_sum(int n) {
   int sum = 0;
   int i = 1;
   int j = 1;
   for (; i <= n; ++i) {
     j = 1;
     for (; j <= n; ++j) {
       sum = sum +  i * j;
     }
   }
}

这段代码的时间复杂度就是O(n^2)。

234 行代码,每行都需要 1 个 unit_time 的执行时间,
第 56 行代码循环执行了 n 遍,需要 2n * unit_time 的执行时间,
第 78 行代码循环执行了 n^2遍,所以需要 2n^2 * unit_time 的执行时间。
所以,整段代码总的执行时间 T(n) = (2n^2+2n+3)*unit_time。

n是一个可以取无穷大的未知数,相对于N^2来说,2n+3微不足道,所以舍去,而 2N^2N^2则可以同化表示为N^2

我们在分析一个算法、一段代码的时间复杂度的时候,也只关注循环执行次数最多的那一段代码就可以了。这段核心代码执行次数的 n 的量级,就是整段要分析代码的时间复杂度。

写多了就有经验了,这一部分也不是今天的主题重头戏,只是个开胃菜而已。

空间复杂度的计算方法亦如是,只是把时间换成了算法消耗的空间了,表示算法的存储空间与数据规模之间的增长关系。


几种常见的复杂度:
在这里插入图片描述


加餐

最好、最坏、平均复杂度

接下来的内容,有的博主就不讲了,比较好的算法数构书里会有,我去找出来的。

// n 表示数组 array 的长度
int find(vector<int> array, int n, int x) {
  int i = 0;
  int pos = -1;
  for (; i < n; ++i) {
    if (array[i] == x) {
       pos = i;
       break;
    }
  }
  return pos;
}

这是一个地毯式搜索的查找函数。
它的时间复杂度是多少呢?

我们在数组中查找一个数据,并不需要每次都把整个数组都遍历一遍,因为有可能中途找到就可以提前结束循环了。但是,

最好情况时间复杂度就是,在最理想的情况下,执行这段代码的时间复杂度。
最坏情况时间复杂度就是,在最糟糕的情况下,执行这段代码的时间复杂度。

自己发挥想象力。


写这么一个算法,肯定是希望它能够复用的,在大量使用的情况下,我们用什么来衡量一段代码的复杂度,对,平均值。
没有想到平均值的自我反省一下,是不是有在思考哦?

这个值就是概率论中的加权平均值,也叫作期望值,所以平均时间复杂度的全称应该叫加权平均时间复杂度或者期望时间复杂度。

用脚指头想都知道,这个算法的平均值是n/2嘛。


均摊时间复杂度

放码过来:

 int array[] = new int[n];
 int count = 0;
 
 void insert(int val) {
    if (count == len(array)) {
       int sum = 0;
       for (int i = 0; i < len(array); ++i) {
          sum = sum + array[i];
       }
       array[0] = sum;
       count = 1;
    }
 
    array[count] = val;
    ++count;
}

# ...............................................................................................

这是一个插入的代码,当数组满了之后,我们用 for 循环遍历数组求和,并清空数组,将求和之后的 sum 值放到数组的第一个位置,然后再将新的数据插入。

那这段代码的时间复杂度是多少呢?

思考一下。

是 O(1)。

对于 insert() 函数来说,O(1) 时间复杂度的插入和 O(n) 时间复杂度的插入,出现的频率是非常有规律的,而且有一定的前后时序关系,一般都是一个 O(n) 插入之后,紧跟着 n-1 个 O(1) 的插入操作,循环往复。

针对这种特殊的场景,我们引入了一种更加简单的分析方法:摊还分析法,通过摊还分析得到的时间复杂度我们起了一个名字,叫均摊时间复杂度。

每一次 O(n) 的插入操作,都会跟着 n-1 次 O(1) 的插入操作,所以把耗时多的那次操作均摊到接下来的 n-1 次耗时少的操作上,均摊下来,这一组连续的操作的均摊时间复杂度就是 O(1)。这就是均摊分析的大致思路。

后面讲到一些比较高深的数据结构的时候会经常用到这种分析法。

(完)