时间复杂度和空间复杂度
时间复杂度
时间复杂度的三种情况
- 最坏情况:任意输入规模的最大运行次数(上界)
- 平均情况:任意输入规模的期望运行次数
- 最好情况:任意输入规模的最小运行次数(下界)
计算机复杂度的两条规则
- 加法规则:多项相加,只保留最高阶的项,且系数变为1。
- T(n,m) = T1(n) + T2(m) = O[max(f(n),g(m))]
- 乘法规则:多项相乘,都保留。
- T(n,m) = T1(n) * T2(m) = O[f(n)*g(m)]
案例1-复杂度相加运算
该例中有两个for循环,循环的上限分别为M+1次和N+1次(+1是因为跳出循环时还计算了一次),由大O的渐进表示法可以计算为:O(M+N+2) 近似为O(M+N),本题没有其他条件,不知道M和N的数量级大小,因此O(M+N)可以作为最终答案。
- 当 M>>N时:时间复杂度为O(M)
- 当 N>>M时:时间复杂度为O(N)
案例2 - 用1代替所有常数次
该案例中有一个for循环,循环的上限取决于条件判断的常数大小,此处是100,说明最多进行101次循环;如果这里的100改为100000000,也同样是进行常数次循环。因此依照大O的渐进表示法,用1替代所有常数,则其时间复杂度为O(1)。
案例3 - strchr采用最坏时间复杂度
strchr函数功能为在一个串中查找给定字符的第一个匹配之处。函数原型为:
char *strchr(const char *str, int c);
即在参数 str 所指向的字符串中搜索第一次出现字符 c(一个无符号字符)的位置。
案例4 - 冒泡排序时间复杂度
最好情况:所有数字已经按序排列,自身有序排列,只需要冒泡排序一遍,由于没有发生交换,因此exchange值不发生改变,仍然为0,最后跳出循环,因此一共就执行了(N-1)次交换,所以其时间复杂度为O(N)。
最坏情况:所有数字倒序排列,每一轮都需要冒泡排序,第一遍冒泡排序(N-1)次,将最大的数字安排到最后一位,然后继续从头开始进行冒泡排序,此时只需要交换(N-2)次,将次大的数安排在倒数第二位,按此顺序一直计算到最后交换1次,此时所有数字排序完成,总共排序了 [(N-1)+(N-2)+·····+ 2 + 1 ] 次,最终计算的时间复杂度为。
案例5 - 二分查找的时间复杂度
二分查找法:这里假设数组元素呈升序排列,将n个元素分成个数大致分为相同的两半,取a[n/2]与查找的x作比较,如果x=a[n/2]则找到x,算法终止;如 果x<a[n/2],则我们只要在数组a的左半部继续搜索x;如果x>a[n/2],则我们只要在数组a的右半部继续搜索x。
最好情况:直接1次就找到了,常数次找到,因此时间复杂度为O(1)。
最坏情况:当找不到该数时,需要不断进行折半,一直折半到最后 begin==end 时,退出循环,此时一共执行了logN次(具体计算步骤见下图)。
案例6 - 递归阶乘的时间复杂度
-
单次递归函数调用为O(1),就看它递归的次数:
本题在疯狂进行递归,每一次递归中包含了常数次的运算:1次逻辑判断运算和乘法运算,但从时间复杂度角度来看可以近似忽略为O(1)次;而真正占据时间复杂度的应当是其向下递归的次数,其递归次数取决于N,是幂函数,因此其时间复杂度为O(N)。
-
单次递归函数调用不为O(1)时,就看它递归调用次数的累加:
long long Fac(size_t N)
{
if (0 == N)
return 1;
for (size_t i = 0; i < N; ++i)
{
printf("%d", i);
}
printf("\n");
return Fac(N - 1) * N;
}
对于上面这种变式情况,每次递归调用不是O(1),每次递归调用中的运算次数是变化的,第一次递归时,函数内部进行N次循环;递归到第二次时,函数内部进行N-1次循环;···;递归到第N-1次时,函数内部循环2次;通过计算我们可以得出:
案例7 - 斐波那契数列的时间复杂度
斐波那契数列的递归其实像是一个N-1层金字塔(当然是有残缺的金字塔),但我们计算复杂度时候真正关心的是数量级问题,残缺的那部分根本不会改变数量级,因此可以将其近似看成一个满元素的金字塔,因此从顶向下挨个计算,也就是计算一个等比数列求和,公比为2;最终计算出其时间复杂度为 :
案例8 - 三层循环的时间复杂度
for (i=1 ;i<=n ;i++)
for(j=1 ;j<=i; j++)
for(k=1; k<=j; k++)
x++;
对于上题我们可以从内层向外层来看,首先最内层循环的次数为 j 次,次外层循环为 i 次,此时计算内部两层循环的次数为(1+2+3+4+·····+i)次,即 (1+i)*i/2 次,这时我们再计算最外层循环,由于最外层循环n次,i的取值范围为(1~n),因此其执行总次数为:
案例9
案例10
空间复杂度
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度 。空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟时间复杂度类似,也使用大O渐进表示法。
案例1 - 冒泡排序空间复杂度
空间复杂度计算的是在程序运行过程中,为了满足程序需求,临时创建的空间个数。在下例冒泡排序中,总共开了三个空间,分别是变量end、exchange、i;尽管在这个循环中,exchange这个变量被循环使用,但是用的始终都是exchange这1个空间,只是使用次数在增加,使用的空间个数始终为1,没有发生改变,因此其时间复杂度为O(N),而空间复杂度为O(1)。
案例2 - 斐波那契数列的空间复杂度
下例中,我们单单计算fibArray动态开辟的空间就已经有(n+1)个了,其他剩下的变量开设的都是常数个,因此时间复杂度为O(N);我们这里只需要算一个大概的即可,计算开辟空间数最多的那个即可。
long long* Fibonacci(size_t n)
{
if(n==0)
return NULL;
long long * fibArray = (long long *)malloc((n+1) * sizeof(long long));
fibArray[0] = 0;
fibArray[1] = 1;
for (int i = 2; i <= n ; ++i)
{
fibArray[i] = fibArray[i - 1] + fibArray [i - 2];
}
return fibArray;
}
案例3 - 计算阶乘递归Fac的空间复杂度
long long Fac(size_t N)
{
if(N == 0)
return 1;
return Fac(N-1)*N;
}
对于本题,递归出N个栈帧,栈帧开辟的空间是常数个,可以认为是O(1),因此决定其开辟空间个数的主要取决于其递归的次数,本题递归次数为N次,因此空间复杂度为O(N)。
案例4 - 计算斐波那契数列的空间复杂度
long long Fib(size_t N)
{
if(N < 3)
return 1;
return Fib(N-1) + Fib(N-2);
}
上例中我们不断调用,当我们一直递归调用到Fib(2)时,将Fib(2)的值返回给Fib(3),这时Fib(2)栈帧就销毁了,Fib(3)将接着调用Fib(1),这时Fib(1)使用了之前Fib(2)使用的空间,使用了同一块空间;同理在向上返回的时候将不断重复使用销毁过的空间,从Fib(2)到Fib(N)总共建立了N-1个栈帧;而后该开始递归调用Fib(N-2),而这块空间正是之前Fib(N-1)使用过的空间,即空间是可以回收复用的。因此本题的空间复杂度为O(N)
程序证明:
当调用完f1( )函数后,将f1中的空间释放;调用**f2( )**函数,由a和b的地址相同可见,f2( )使用的空间正是刚刚f1释放的空间,证明空间被重复利用了。
void f1()
{
int a = 0;
printf("%p\n", & a);
}
void f2()
{
int b = 0;
printf("%p\n", & b);
}
int main()
{
f1(); //调用函数开始会创建栈帧;结束会销毁栈帧
f2();
return 0;
}