一.时间复杂度
1.定义
定义:所有语句频度之和,记为T(n)。一个语句频度是指该语句在算法中被重复执行的次数。
T(n)是算法中问题规模的函数,时间复杂度主要分析T(n)数量级。
同通常采用算法中基本运算频度f(n)来分析算法时间复杂度。取f(n)中随n增长最快的项。
算法时间复杂度记为T(n)=O(f(n))。
理解T(n)=O(f(n)):O(f(n))表示取f(n)的同阶无穷小。f(n)的同阶无穷小就是取函数每项对比之后阶数最高的。
为什么要这么取呢?时间复杂度是大概描述算法效率,不需要多精确,所以,计算的时候直接取函数中阶数最高的部分,其余部分忽略不计。
2.三种时间复杂度
最坏时间复杂度:考虑输入数据最坏的情况
平均时间复杂度:考虑所有输入数据等概率出现的情况。
最好时间复杂度:考虑输入数据最好的情况。
3.计算
*计算
若语句执行次数是一个常数,则时间复杂度为O(1)。
下面是针对语句执行次数不是常数。
#计算步骤
- 找基本操作 :就是找最深层循环。
- 找问题规模n:出现在控制循环的条件中 ,问题规模n限制循环上限。
- 找控制循环的变量 i,分析循环变量的变化量。
- 根据2和3步骤分析基本操作执行次数x与问题规模n的关系f(n)=x。
- 取x的数量级O(x)(或者Of(n))就是算法时间复杂度T(n)。
【做题做熟悉了,一般把第三步和第四步合在一起,直接分析问题规模 n 和循环次数 x 的关系式。】
#常用技巧
*加法规则:
O(f(n))+ O(g(n)) = O(max(f(n),g(n)));
该方法出现在“语句块 一个模块一个模块”的情况。
*乘法规则:
O(f(n))*O(g(n)) = O((f(n)*g(n)));
该方法出现在“语句嵌套”和“函数递归调”用中。但不是所有嵌套语句都适用。
*常见的渐进时间复杂度:
O(1) < O(log2 n) < O(n) <O(n*log2 n) <O(n2) <O(n3) <O(2n) <O(n!) <O(nn)
#例题解析
上例题:
第一道例题
void fun(int n)
{
int i = 1;
while(i <= n)
{
i = i * 2;
}
}
第一步:找基本操作
便是语句 while循环
第二步:找问题规模
问题规模出现在条件 i <= n 中,那就是n。
第三步:找循环控制的变量 i 。
i每循环一次都要在自身乘以2。每次循环 i 值变化:2 4 8 16 ………。设循环x 次,则这里可以写出关于 i 的变化值与循环次数的函数 i = 2x。
第四步:建立循环次数 x 与 问题规模n的关系。
因为i <= n,根据第三步建立关系 2x= n ;
解得 x =log2 n。
第五步:取数量级
O(x)= log2 n。
则时间复杂度T(n) = log2 n。
第二道例题:
int m = 0,i,j;
for(i = 1;i <= n;i++) //第一层
for(j = 1;j <= 2*i ;j++) //第二层
m++;
嵌套语句从外层往内层分析
先分析第一层(虽然一眼看出来,但是还是浅浅分析一下)
for(i = 1;i <= n;i++)
第一步:找基本操作
那就是这个两层的嵌套循环。
【插入一个额外值得注意的,为什么要说找最深层循环,当在程序中出现多个语句模块时,就要把每个模块都计算相加,就比较费时间,对比一下语句模块,谁嵌套层数越多就越复杂,计算最复杂的,也就所说的找最深层循环,定位到最复杂的语句模块后,分析嵌套时就不用再进行第一步,每层直接从找问题规模开始】
第二步:找问题规模n
问题规模出现在条件 i <= n 中,那就是n。
第三步:找循环控制的变量 i 。
i 初始值为 1,i 增量为 1 ,设循环次数为 x1 则没次循环 i 与x1 的关系为 i = x1
第四步:建立循环次数 x 与 问题规模n的关系。
因为i <= n,根据第三步建立关系 x1 = n ;
分析第二层:
for(j = 1;j <= 2*i ;j++)
1.找问题规模n
该语句中问题规模为 2 * i ,(当我们第二层循环的问题规模与第一层相关联时,外层乘以内层循环计算时间复杂度是不可行的,我们应该回到时间复杂度的定义即语句执行的次数)每执行一次第一层循环,第二层循环问题规模变化 :2 ,4,6,8… ;
2.找控制循环的变量
该语句控制循环的变量为 j ,增量为1,那么内层循环结束语句执行次数可用下列式子表示:
综合分析取数量级
综合内外层外层循环结束语句执行次数可用下列式子表示:
语句执行次数为 f(n) = n2 +n;
T(n) = O(f(n)); T(n) = n2;
插入一道类似的题:
int m = 0,i,j;
for(i = 1;i <= n;i*=2) //第一层
for(j = 1;j <= i ;j++) //第二层
m++;
分析第一层:
for(i = 1;i <= n;i*=2)
第一步直接略过到第二步。
第二步:找问题规模n
问题规模出现在条件 i <= n 中,那就是n。
第三步:找循环控制的变量 i
i 初始值为 1,i 增量为自身乘以2 ,变化为 1,2,4,8,16…,按照 i=2x-1 变化,设循环次数为 x 则没次循环 i 与x 的关系为 i=2x-1 变化。
第四步:建立循环次数 x 与 问题规模n的关系。
因为i <= n,根据第三步建立关系 i = n ;故 2x-1 = n ;x = log2n +1;1忽略不计。
故 x = log2n ;
分析第二层:
for(j = 1;j <= i ;j++)
1.找问题规模n
该语句中问题规模为 i ,第二层循环问题规模变化由第一层对 i 的分析可知 i=2x-1 。
2.找控制循环的变量
该语句控制循环的变量为 j ,增量为1,那么内层循环结束语句执行次数可用下列式子表示:
综合分析取数量级
综合内外层外层循环结束语句执行次数可用下列式子表示:
语句执行次数为f(n) = 2*n-1;
T(n) = O(f(n)); T(n) = n;【注意注意:系数也是不计的】
第三道列题
int fact(int n)
{
if(n <= 1 )
return 1;
return n * fact(n-1);
}
【对于函数的递归调用,问题规模定位在传入的参数里,主要分析的是问题规模与函数调用次数的关系。再分析函数内部的时间复杂度,把内部时间复杂度与函数调用次数相乘,取数量级,就是该递归程序的时间复杂度。】
找问题规模n
问题规模在 int fact(int n) 中,则问题规模为 n。
调用次数 x 与问题规模 n的关系
问题规模变化量每次 n -1,题中条件语句n <= 1时,递归调用结束,则 x = n;
取数量级
函数内部时间复杂度为O(1) ,内部时间复杂度与函数调用次数相乘得到最后时间复杂度T(n) = n;
【个人小结:具体问题具体分析,再计算时间复杂度越来越熟练,直接分析问题规模 n 与执行次数 x 的关系。计算步骤适用于大部分程序。主要还是要抓住时间复杂度是语句执行的次数。】
二.空间复杂度
#定义
一个程序执行时所需要的存储空间来存放本身所用指令、常数、变量和输入数据外,还需要对一些数据进行操作的工作单元和存储实现计算机所需信息的辅助空间。
算法空间复杂度S(n) 为该算法消耗的存储空间。
首先,程序代码是固定不变的,不算进空间复杂度。
程序中那个部分能算进空间复杂度?
程序中数据部分算进空间复杂度。
数据:数据的初始化:如 int i; 整形的 i 占了四个字节,就是4b。
*计算
#计算步骤
第一步:找问题规模
第二步:找数据,分析占用空间与问题规模的关系。
第三步:取数量级
#常用技巧
*加法规则:
O(f(n))+ O(g(n)) = O(max(f(n),g(n)));
该方法出现在“不同的数据”的情况。
不同的数据,比如 int a,b; 这里把a 和 b 所占空间相加,a 和 b 是不同的
数据。
*乘法规则:
O(f(n))*O(g(n)) = O((f(n)*g(n)));
该方法出现在“二维数组”以上的数组。
*常见的渐进时间复杂度:
O(1) < O(log2 n) < O(n) <O(n*log2 n) <O(n2) <O(n3) <O(2n) <O(n!) <O(nn)
#例题解析
上例题:
【下面例题中会出现int i,i 占4B。根据不同的编译器,不同的数据类型占用空间是不一样的,在学习数据结构C语言版中,int 类型在C语言的内存中占四个字节,char 在C语言中占两个字节,为了方便理解,int i ,i 可以看为占了一个空间】
第一道例题:
void PRINT(int n)
{
int i = 1;
while(i <= n)
{
i++;
printf("第%d位选手");
}
}
第一步:找问题规模
代码中问题规模为 n
第二步:找数据,
代码中数据为 int i 。占了 4B 。
第三步:取数量级
无论问题规模怎么变,算法运行所需要的内存空间是固定的“常量”。常量都记作O(1)。所以 S(n) =O(1)
第二道例题:
void fun(int n)
{
int s[n],k[n][n];
}
第一步:找问题规模
代码中问题规模为 n
第二步:找数据
代码中数据为 s[n],k[n][n]。f(n) 为 s 数组所占空间与 k 数组所占空间之和。
所以 f(n) = n2 + n 。
第三步:取数量级
f(n) = n2 + n ,S(n) =O(f(n)), 则S(n) = O(n2 )。
第三道例题
递归中的空间复杂度。
关于递归空间的调用,先了解函数调用栈。递归调用没有结束,空间不会被释放而是而是以栈的逻辑结构方式存在内存里,直到递归调用结束,空间才会释放。
void PRINT(int n)
{
int a ,b ,c;
if(n > 1)
{
PRINT(n - 1);
}
printf("第%d位选手\n",n);
}
第一步:找问题规模
代码中问题规模为 n
第二步:找数据
代码中数据为 int a ,b ,c。每次递归调用都要占 12B, 调用递归 n 次 ,则空间复杂度为 n。
第三步:取数量级
由第二步可得 T(n) =O(n);
扩展
void PRINT(int n)
{
int f[n];
if(n > 1)
{
PRINT(n - 1);
}
printf("第%d位选手\n",n);
}
第一步:找问题规模
代码中问题规模为 n
第二步:找数据
代码中数据为 int f[n] ,n 在递减,变化从 n ,n-1, n-2,…1。反过来 变化为 1,2,3,4…n -2,n-1,n;
第一次调用占n个空间,第二次调用占n-1个空间…最后一次调用占1个空间。是个等差数列,对这个等差数列求和得:g(n) = 1/2 * n2 + 1/2 * n;
第三步:取数量级
由第二步得关系:g(n) =1/2 * n2 + 1/2 * n; S(n) = O(g(n)), S(n) = O(n2)
【个人小结:具体问题具体分析,有些算法各层所需的存储空间不同,分析就略有区别。】