如何分析、统计算法的执行效率和资源消耗?
- 执行效率是算法一个非常重要的考量指标。
- 时间、空间复杂度分析。
为什么需要复杂度分析
- 1、测试结果非常依赖测试环境
- 2、测试结果受数据规模的影响很大
- 我们需要一个不同具体的测试数据来测试,就可以粗略地估计算法的执行效率的方法。
大O复杂度表示法
- 粗略估计,每行代码执行时间都一样,为unit_time。
- 第2,3行代码分别需要1个unit_time的执行时间,第4,5行都运行了n遍,所以需要2n * unit_time的执行时间,所以总的执行时间为(2n+2)* unit_time。
- 第2、3、4行代码,每行都需要1个unit_time的执行时间,第5、6行代码循环执行了n遍,需要2 * unit_time的执行时间,第7、8行代码循环执行了n2遍,所以需要2n2 * unit_time的执行时间。所以总的执行时间为T(n) =(2n^2 + 2n + 3)* unit_time。
规律:所有代码的执行时间T(n)与每行代码的执行次数成正比。
- T(n)表示代码执行的时间;n表示数据规模的大小;f(n)表示每行代码执行的次数总和。公式中的O,表示代码的执行时间T(n)与f(n)表达式成正比。
- 所以,第一个例子中的T(n) = O(2n + 2),这就是大O时间复杂度表示法。
- 大O时间复杂度代表代码执行时间随数据规模增长的变化趋势;所以,也叫作渐进时间复杂度。
时间复杂度分析
1、只关注循环执行次数最多的一段代码
- 大O复杂度表示法只是表示一种变化趋势。通常会忽略掉公式中的常量、低阶、系数,只需记录一个最大阶的量级就可以了。
- 我们在分析一个算法、一段代码的时间复杂度的时候,也只关注循环执行次数最多的那一段代码就可以了。
2、加法法则:总复杂度等于量级最大的那段代码的复杂度
- 第一段代码循环执行了100次,是一个常量的执行时间,跟n的规模无关。即便这段代码循环了100000次,对代码的执行时间来说会有很大影响,但时间复杂度的概念表示的是一个算法执行效率与数据规模增长的变化趋势,所以不管常量的执行时间多大,都可以忽略掉,因为它本身对增长趋势并没有影响。
- 综合这三段代码的时间复杂度,我们取其中最大的量级。所以,整段代码的时间复杂度就为O(n^2)。
- 总的时间复杂度就等于量级最大的那段代码的时间复杂度。
- 如果T1(n) = O(f(n)),T2(n) = O(g(n));那么T(n) = T1(n) + T2(n) = max(O(f(n)),O(g(n))) = O(max(f(n),g(n)))。
3、乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积
- 整个cal()函数的时间复杂度就是T(n) = T1(n) * T2(n) = O(n * n) = O(n^2)。
几种常见的时间复杂度实例分析
- 粗略分为两类,多项式量级和非多项式量级。其中,非多项式量级有:O(2^n)和O(n!)。
- 当数据规模n越来越大时,非多项式量级算法的执行时间急剧增加。所以,非多项式时间复杂度的算法是非常低效的算法。
多项式时间复杂度
1、O(1)
- 一般情况下,只要算法中不存在循环语句、递归语句,即使有成千上万行的代码,其时间复杂度也是O(1)。
2、O(logn)、O(nlogn)
- 根据前面讲的时间复杂度分析方法,第三行代码是循环执行次数最多的。所以计算出这行代码被执行多少次,就知道这段代码的时间复杂度。
- 求x值,x = log2n,所以,这段代码的时间复杂度就是O(log2n)。
- 对数之间是可以互相转换的,log3n就等于log32 * log2n,所以O(log3n) = O(C * log2n),其中C是常量。
3、O(m+n)、O(m*n)
- 代码的复杂度由两个数据规模来决定。
- 由于不知m和n的量级谁大,所以不能简单地利用加法法则,省略掉其中一个。上面的时间复杂度就是O(m + n)。
- 针对这种情况,加法规则改为:T1(m) + T2(n) = O(f(m) + g(n)),乘法法则依旧为:T1(m) * T2(n) = O(f(m) * f(n))。
空间复杂度
- 空间复杂度全称是渐进空间复杂度,表示算法的存储空间与数据规模之间的增长关系。
- 第2行代码中,申请了一个空间存储变量i,但它是常量阶,跟数据规模n没有关系,我们可以忽略。第3行申请了一个大小为n的int类型数组,除此之外,剩下的代码都没有占用更多的空间,所以整段代码的空间复杂度就是O(n)。
- 常见的空间复杂度就是O(1)、O(n)、O(n^2),像O(logn)、O(nlogn)这样的对数阶复杂度平时都用不到。
最好、最坏情况时间复杂度
- 时间复杂度O(n)
- 最好时间复杂度O(1)
最好情况时间复杂度就是在最理想的情况下,执行这段代码的时间复杂度。
最坏情况时间复杂度就是在最糟糕的情况下,执行这段代码的时间复杂度。
平均情况时间复杂度
- 要查找的变量x在数组中的位置,有n + 1种情况:在数组的0 ~ n - 1位置中和不在数组中。
- 我们把每种情况下,查找需要遍历的的元素个数累加起来,然后再除以n + 1,就可以得到需要遍历的元素个数的平均值,即:
- 从中可以得出平均时间复杂度就是O(n),这个结论虽然是正确的,但计算过程有问题。
- 要查找的变量x,要么在数组里,要么不在数组里。假设在数组中与不在数组中的概率都为1/2。要查找的数据出现在0n-1这n个位置的概率也是一样的,为1/n。所以,根据概率乘法法则,要查找的数据出现在0n-1中任意位置的概率就是1/(2n)。
- 这个值就是概率论中的加权平均值,也叫作期望值,所以平均时间复杂度的全称应该叫加权平均时间复杂度或者期望时间复杂度。
均摊时间复杂度
- 这段代码实现了一个往数组插入数据的功能。当数组满了以后,也就是代码中的count==array.length时,我们用for循环遍历数组求和,并清空数组,将求和之后的sum值放到数组的第一个位置,然后再将新的数据插入。但如果数组一开始就有空闲空间,则直接将数据插入数组。
- 最理想的情况下,数组中有空闲空间,只需将数据插入到数组下标为count的位置就可以了,所以最好情况时间复杂度为O(1)。
- 最坏的情况下,数组中没有空闲空间了,我们需要先做一次数组的遍历求和,然后将数据插入,所以最坏情况时间复杂度为O(n)。
- 平均时间复杂度为O(1)。
- 对比前面的例子,find()函数在极端情况下,复杂度才为O(1)。但insert()在大部分情况下,时间复杂度都为O(1)。只有个别情况下,复杂度才比较高,为O(n)。这是insert()第一个区别于find()的地方。
- 第二个不同的地方。对于insert()函数来说,O(1)时间复杂度的插入和O(n)时间复杂度的插入,出现的规律是非常有规律的,而且有一定的前后时序关系,一般都是一个O(n)插入之后,紧跟着n-1个O(1)的插入操作,循环往复。
- 针对这种特殊的场景,引入一种更加简单的分析方法:摊还分析法。
- 每一次的O(n)的插入操作,都会跟着n-1次O(1)的插入操作,所以把耗时多的那次操作均摊到接下来的n-1次耗时少的操作上,均摊下来,这一组连续的操作的均摊时间复杂度就是O(1)。
- 均摊时间复杂度就是一个特殊的平均时间复杂度。