1、复杂度分析(学习笔记)
1. 为什么需要复杂度分析
统计代码的执行时间和占用的内存,完全可以通过统计、监控来得到,为什么还要做时间、空间复杂度分析呢?
其实,这种统计方法在一些算法书籍中叫做事后统计法。但是这种方法的局限性很多。
-
在实际测试环境中,硬件的不同对测试结果有很大的影响。
-
测试数据受数据规模的影响。
时间、空间复杂度分析的作用就是不用具体的测试数据,就可以粗略的估计算法的执行效率的方法。
2. 大O复杂度表示法
算法的执行效率,粗略的讲,就是算法代码执行的时间。
下面是一段简单的C语言代码
int cal(int n)//求阶乘
{
int result=1;
for(int i=1;i<=n;i++)
{
result *= i;
}
return result;
}
我们假设处理器执行每行代码的时间为unitTime,第3、8行执行了一次,第4、6行执行了n次,那么这个函数总执行时间为
T
(
n
)
=
(
2
n
+
1
)
×
u
n
i
t
T
i
m
e
T(n) = (2n+1)×unitTime
T(n)=(2n+1)×unitTime
代码的执行时间T(n)与代码执行次数成正比。
那么下一段代码:
int cal(int n)
{
int sum=0;
for(int i=0;i<n;i++)
{
for(int j=0;j<n;j++)
{
sum+=i*j;
}
}
return sum;
}
那么T(n)
T
(
n
)
=
(
2
n
2
+
2
n
+
1
)
×
u
n
i
t
T
i
m
e
T(n) = (2n^2+2n+1)×unitTime
T(n)=(2n2+2n+1)×unitTime
由以上两个例子可以得出,代码执行时间T(n)与代码执行次数n成正比。所以
T
(
n
)
=
O
(
f
(
n
)
)
T(n)=O(f(n))
T(n)=O(f(n))
大O时间复杂度实际上并不表示代码的执行时间,而是表示代码执行时间随数据规模的增长的变化趋势,所以叫做渐进时间复杂度(asymptotic time complexity),简称时间复杂度。
当n很大时,公式中的低阶、常量、系数三部分并不改变增长的趋势,所以可以忽略。只需要记录最大量级就可以了。所以刚才的例子可以记为
1
、
T
(
n
)
=
O
(
n
)
1、T(n)=O(n)
1、T(n)=O(n)
2. 、 T ( n ) = O ( n 2 ) 2.、T(n)=O(n^2) 2.、T(n)=O(n2)
3. 时间复杂度分析
- 只关注循环代码执行次数最多的一段代码
- 加法法则:总复杂度等于量级复杂度最大的那段代码的复杂度
- 乘法法则:嵌套代码的复杂度等于嵌套内外代码都复杂度的乘积
4. 几种常见时间复杂度实例分析
常量阶 | O(1) |
对数阶 | O(logn) |
线性阶 | O(n) |
线性对数阶 | O(nlogn) |
k次方阶 | O(n^k) |
指数阶 | O(2ⁿ) |
阶乘阶 | O(n!) |
可分为两类:
-
多项式量级
-
非多项式量级:O(2ⁿ) ,O(n!)
时间复杂度为非多项式量级的算法问题叫做NP(Non-Deterministic Polynomial,非确定多项式)问题
下面为举例
//O(1)
int a=5,c=9;
int result = a+c;
//O(logn)
int k=1;
while(k<n)
{
k*=2;
}
2 x = n , 那 么 x = l o g 2 n 2^x=n,那么 x=log_2{n} 2x=n,那么x=log2n
不管以哪个数作为对数的底数,都以O(logn)来标志,对数的底数可以相互转换例如
l
o
g
2
n
=
l
o
g
3
n
/
l
o
g
3
2
log_2{n}=log_3{n}/log_3{2}
log2n=log3n/log32
那么:
l
o
g
3
n
=
l
o
g
3
2
×
l
o
g
2
n
log_3{n}=log_3{2}×log_2{n}
log3n=log32×log2n
空间复杂度分析
前面说过时间复杂度全称为渐进时间复杂度,表示算法的执行时间与数据规模之间的增长关系。那么类比一下,空间复杂度全称为渐进空间复杂度(asymptotic space complexity),表示算法的存储空间与数据规模之间的增长关系。
例如:
void demo(int n)
{
int *p = (int*)malloc(sizeof(int)*n);
for(int i=0;i<n;i++)
{
p[i]=i+1;
printf("%d ",p[i]);
}
}
第三行申请了一个大小为n的int类型的数组,空间复杂度为O(n)。
空间复杂度为O(logn)例子
以二进制形式存储一个十进制数n,所占bit与n的关系
O ( l o g n ) b i t O(logn) bit O(logn)bit
5. 时间复杂度分析++
- 最好情况时间复杂度
- 最坏情况时间复杂度
- 平均情况时间复杂度
- 均摊时间复杂度
下面一个函数 ,查找数组中的值的索引
int Fine(vector<int> arr,int value)
{
int pos=-1;
for(int i=0;i<arr.size();i++)
{
if(arr[i] == value)
{
pos=i;
break;
}
return pos;//没找到,返回-1
}
}
在这段代码中,用上面的分析方法并不适用,因为查找的值的不同,循环的次数也是不一样的。
那么引入三个概念:最好情况时间复杂度、最坏情况时间复杂度、平均情况时间复杂度。
如果要查找的value就在数组中的第一个位置,则此种情况为最好的情况,时间复杂度为O(1)。
如果要查找的value不在数组中,则此种情况为最坏的情况,时间复杂度为O(n)。
平均情况时间复杂度,根据概率论的知识,为了方便理解,假设value在数组内和不在数组内的概率相等都为0.5,要查找的数据出现在0~n-1的位置的概率也是一样的,为1/n。所以,要查找的value出现在数组任意位置的概率为1/(2n)。那么平均时间:
1
×
(
1
/
2
n
)
+
2
×
(
1
/
2
n
)
+
3
×
(
1
/
2
n
)
+
.
.
.
+
n
×
(
1
/
2
n
)
+
n
×
(
1
/
2
)
=
(
3
n
+
1
)
/
4
1×(1/2n)+2×(1/2n)+3×(1/2n)+...+n×(1/2n)+n×(1/2)=(3n+1)/4
1×(1/2n)+2×(1/2n)+3×(1/2n)+...+n×(1/2n)+n×(1/2)=(3n+1)/4
(3n+1)/4就是加权平均值,也就是期望值,那么用大O表示法为O(n)。
均摊复杂度
先看一段代码
vector<int> arr(n); //长度为n的数组
int count=0;
void push(int val)
{
if(count==arr.size())
{
for(int i=1;i<arr.size();i++)
{
arr[0]+=arr[i];
arr[i]=0;
count=1;
}
}
arr[count++]=val;
}
该段代码,在push函数中,向数组arr中添加数据,如果数组不满,直接放在尾部,如果数组满了,将所有元素的和放在第一个位置,清空其他位置的数据。
该函数出现最坏情况的频率时有规律的,每n-1次push数据,下一次会出现最坏情况。也就是每n-1次O(1)都会紧跟着一次O(n)。
针对这种情况,有一种简单的分析方法:摊还分析法,通过摊还分析法得到的时间复杂度叫做均摊时间复杂度。
如何计算均摊复杂度?
每n-1次O(1)都会紧跟着一次O(n),把耗时多的那一次操作均摊到前n-1次中去,那么该n次操作的均摊时间复杂度为O(1)。