为什么需要复杂度分析
对于一段算法,如何判定它是否高效?如何针对现有的数据量级对于这段代码以及数据增长之后的空间、时间进行预估?可能有人会认为,这个问题很简单,直接写一段测试代码,实际运行一下就知道了。这种统计的方法叫做:事后统计法
单纯的事后统计法存在几点问题:
- 统计结果完全依赖于你的硬件环境。例如你用i8的内核肯定比你i3内核的机器运行的快。
- 测试结果收数据量级规模以及规则影响很大。例如排序算法,如果已经有序,消耗时间可以忽略不计。
- 当需要很大的数据量级去测试的时候,可能及其消耗时间与经历。并且测出来的可能压根不准确。而有很多时候我们只是需要去预估的时候也许压根也没有这么大的数据去测试。
因此我们就需要有一个能单纯统计这段算法效率、空间复杂度的方法,也就是这里的算法复杂度。
什么是算法复杂度
**注意:**算法复杂度并不是代表一段代码实际执行的时间与实际消耗的空间,只是表示代码执行时间/空间随数据规模增长的变化趋势。因此全称为渐进时间/空间复杂度。
算法复杂度又分为两块:
- 时间复杂度:表示代码执行时间随数据规模增长的变化趋势。
- 空间复杂度:表示代码执行消耗空间随数据规模增长的变化趋势。
时间复杂度分析
1.如何计算?
首先,我们假设每行代码执行时间为固定时间unit_time。我们认为这值是一个常量值,不会随着硬件条件以及数据规模的改变而改变。
func test(n int) int {
a := 0 //1
for i:= 0; i<n; i++ { //2,3
a = a+i //4
}
return a
}
上面是一个最简单的循环,那么按照我们上面所说,实际执行时间就是:unit_time + unit_time + 2n*unit_time,for循环的第一个初始化语句(2)可以认为就执行了一次,因此1、2的时间都是unit_time,之后的判断逻辑(3)与循环里面的逻辑(4),都执行了n次,因此时间为2n*unit_time,那么使用大O表示法就是:T(n) = O(2n+2),当n无限大的时候,我们就可以忽略其余的常数,因此这段算法的的时间复杂度为O(n)。
2.计算时间复杂度的一些技巧
-
忽略常数
- 以上面的例子为例,单位时间的常量,我们均在认为n无限大的情况下,舍弃掉。
-
加法—取最大
func test(n int) int { a := 0 for i:= 0; i<100; i++ { a = a+i } b := 0 for i:= 0; i<n; i++ { b = b+i } c := 0 for i:= 0; i<n; i++ { for j:=0; j<n; j++ { c = c+j } } return a + b + c }
这里有三个循环,最终返回三个数的和,那么就是100*unit_time+n*unit_time+n*unit_time,首先第一个循环循环次数是一个固定值,按照我们上面所说,不管是100、1000、10000均不是一个随着n变化的常量,因此直接舍弃,第二个循环复杂度均为O(n),第三个循环复杂度为O(n*n)因此最终的复杂度为***O(n2)**n的平方。
-
乘法—相乘
上面例子中的第三个循环就是标准的乘法运算,复杂度出现嵌套的情况下,直接相乘即可。
常见时间复杂度量级分析
-
多项式量级
-
常量阶 O(1)
- 一般来说,只要代码中没有循环、递归等,就算其中有无数行代码,时间复杂度也为O(1)
-
对数阶 O(logn)
i := 1 while ( i<= n ) { i = i * 2 }
这样的一个算法复杂度为O(log2n)。
如果我们将i*2改成i*3,那么复杂度为O(log3n)。
忽略系数,时间复杂度则为O(logn),那么**O(nlogn)**则是logn的算法循环了n次。
-
线性阶 O(n)
- 上一节如何计算与计算其中已经介绍了O(n)
-
线性对数阶 O(nlogn)
- **O(nlogn)**则是logn的算法循环了n次
-
平方/立方/K次方阶 O(n2/n3/nk)
- 上一节如何计算与计算其中已经介绍了平方/立方/K次方阶
-
-
非多项式量级
- 指数阶 O(2n)
- 阶乘阶 O(n!)
空间复杂度
空间复杂度和时间复杂度一致,包括计算方法、技巧均与时间复杂度一致。
常用复杂度趋势图
欢迎大家关注我的个人博客:http://blog.geek-scorpion.com/