什么是时间复杂度(一篇搞定时间复杂度)

一、概念

  时间复杂度是一个函数,该函数描述了程序或者算法的运行时间快慢,该函数的表达式是通过计算程序或者算法中基本语句的执行次数得来的,该函数和执行次数成正比,往往时间复杂度是衡量一个算法或者程序好坏的重要标准。

  实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大 O 的渐进表示法:意思就是取函数的最高阶,并且高阶的系数也忽略,O 仅仅是一个符号而已,这个取最高阶并没有多大的影响

概念的剖析:
1、大 O 的渐进表示法怎么用?
如下所示,假如我们计算出了时间复杂函数f(n),其中n只是一个变量符号而已,关于函数是怎么算出来的,先不关心,后面计算部分会讲,然后将它们用大O的渐进表示法表示

在这里插入图片描述

以上就是大O的渐进表示法,我们只需要保留函数的最高阶项,并且去掉系数就行,对于只有常数项的函数如:(4)和(5),我们直接记为O(1),表示复杂度是常数级别,不论常数有多大都是O(1)。因为此时时间复杂度根本不受变量n的影响,所以我们以后对于这种由常数决定的执行次数(或者说由常数决定的时间复杂度),统一记为O(1)

2、保留函数最高阶项对整个函数会不会有影响?
比如: 对函数F(N) = N^2 + N + 10用大O渐进表示法得时间复杂度:O(N^2)
我们可以看一下取高阶后数值的影响:

在这里插入图片描述

对比上面的函数 F(N) 和 O(N^2) 的结果发现:当N取值一样的时候他们都属于同一数量级,并且当N越大, 决定F(N)结果的是N^2项(可以理解为求极限),所以以后时间复杂度的表示直接可以采用O的渐进表示法,其实也可以感受到时间复杂度是一种不精确的估算,这样也给我们后面计算带来了方便,有时可以一眼看出时间复杂度

3、为什么要用基本语句执行次数来描述运行时间快慢?
  我们一般描述算法快慢第一个想到的就是:直接在电脑上跑一下不就知道了吗,但是:
  首先,我们电脑不可能一直带在身上
  其次,有时两种能完成相同功能的不同算法(这两种算法运行效率有很大的差别),我们在计算机上测试出来的时间几乎是一样的,但是你能说他们运行时间快慢一样吗?肯定不能,因为我们测试的数据往往观察不出来差别,需要大数据下才可以(比如常见的排序算法,你可以测试一下,基本你给的数据一下就可以出结果)
  最后,就有人想出来了用基本语句执行次数来描述运行时间快慢解决了上述问题。其实以执行次数为参考他是从算法思想角度出发解决的,而不是依赖于写的算法程序,这就是为什么同一个算法在不同的语言下时间复杂度都是一样的

4、什么是基本语句?
  基本语句可以理解为算法中被执行次数最多的语句,一般直接找算法中最深层次的语句,简单来讲就是:如果有循环,就是最里面那层循环里的语句,如果没有循环,算法里所有的语句都可以看成基本语句

二、计算

计算时间复杂度我下面采用循环渐进的方式讲解:每道题都是一个典型的例题

第一题:计算Func1的时间复杂度

void Func1(int N)
{
	int count = 0;
	// 下面是一个嵌套的循环
	for (int i = 0; i < N ; ++ i) //第一层循环
	{
		for (int j = 0; j < N ; ++ j) //第二层循环
		{
			++count;
		}
	}
	for (int k = 0; k < 2 * N ; ++ k) //一层循环
	{
		++count;
	}
	int M = 10;
	while (M--) //一层循环
	{
		++count;
	}
}

分析:找基本语句,++count就是,因为它是所有循环里面最内层的语句。第一个for循环里面嵌套了个循环,外层循环N次,内层循环N次,总共执行 N * N = N^2 次,第二个for循环只有一层,执行N次,第三个while循环执行 M = 10 次,最终执行 N^2 + N + 10次,所以描述时间复杂度函数可以表示为 F(N) = N^2 + N + 10,最终时间复杂度表示为O(N^2)

第二题:计算Func2的时间复杂度

void Func2(int N)
{
	 int count = 0;
	 for (int k = 0; k < 2 * N ; ++ k)
	 {
		 ++count;
	 }
	 int M = 10;
	 while (M--)
	 {
		 ++count;
	 }
	 printf("%d\n", count);
}

分析:++count就可以被看成基本语句,第一个for循环执行2N次,while循环执行M=10次,时间复杂度函数F(N)=2N+10,时间复杂度最终可以表示为O(N),这里取最高阶也就是2N,并且最高阶的系数可以去掉,所以结果是O(N)

第三题:计算Func3的时间复杂度

void Func3(int N, int M)
{
	 int count = 0;
	 for (int k = 0; k < M; ++ k)
	 {
		 ++count;
	 }
	 for (int k = 0; k < N ; ++ k)
	 {
		 ++count;
	 }
	 printf("%d\n", count);
}

分析:++count可以看成基本语句,第一个for循环执行了M次,第二个for循环执行了N次,时间复杂度函数F(N,M)= N + M,最终表示为O(N+M),这里很奇怪,为什么是N+M而不是一个N或者M,因为N和M我们是不确定的,所以我们都写上,如果告诉你,N远大于M,则是O(N);M远大于N,则是O(M);N和M相等,则是O(N)或者O(M)

第四题:计算Func4的时间复杂度

void Func4(int N)
{
	 int count = 0;
	 for (int k = 0; k < 100; ++ k)
	 {
		 ++count;
	 }
	 printf("%d\n", count);
}

分析:++count可以看成基本语句,里面只有一个for循环,for循环执行了100次,所以F(N)=100,时间复杂度最终可以表示为O(1)

第五题:计算Func5的时间复杂度

//该函数的功能是在字符串str中找字符ch,找到返回下标,找不到返回-1
int Func4 ( char* str, int ch )
{
	int i = 0;
	while(*(str + i) != '\0')
	{
		if(*(str + i) == ch)
		{
			return i;
		}
	}
	return -1;
}

分析:if判断语句就可以看成基本语句,但是我们发现这个函数中并没有我们之前说的N,那么什么来决定循环次数呢?找到这个字符需要对比多少次来决定,此时我们就要分情况(只讨论能找到的前提下,因为找不到也就跟下面最坏的情况差不多):
最好的情况,对比一次就找到了(第一个字符就是你要找的),直接返回结束,对比1次;
最坏的情况,对比到最后一个才找到(相当于你把字符串遍历了一遍),对比了n次(一般对于这种长度不确定的我们都默认是n,以最长的情况考虑,也就是做最坏的打算)
此时有两种情况,我们要选择哪种情况呢?在实际中一般情况关注的是算法的最坏运行情况,所以时间复杂度为O(n),当然你也可以算出平均情况:(1+n) / 2,直接两种情况相加除以2,然后根据平均计算时间复杂度,化简(1+n) / 2 = 0.5n+0.5,取高阶得:O(n),最终结果都一样,我们通常直接看最坏情况就可以
从这道题中你可以感受到计算时间复杂度做的是最坏的打算,保守的预期,我们生活中做事也是往往做最坏的打算,只要我做到了最坏,就没有再坏再差的了

第六题:计算Fac的时间复杂度

//函数是递归函数,功能是计算n的阶乘
long long Fac(int n)
{
	 if(0 == n)
	 {
	 	return 1;
	 }
	 return Fac(n-1)*n;
}

分析:函数没有循环,if判断语句和return语句都可以看作基本语句,函数每次进来都只执行1次,但是你不能直接说该函数的时间复杂度是O(1),因为这是递归函数,return后面还会再次调用自己,函数并没有结束。每次递归n都会减1,所以递归了n次,并且每次递归,函数基本语句执行次数为1,最终将它们累加结果就是n,时间复杂度函数f(n)=n,所以时间复杂度为O(n),总结:递归算法的时间复杂度是:多次调用的基本语句执行次数累加(也就是说每次调用都会有一个基本语句的执行次数,最后将所有的累加起来)

第七题:计算Fac2的时间复杂度

//函数是递归函数,功能是计算n的阶乘,这个函数相对于上一个函数增加了一个for循环,其实这个for并没有什么作用,只是消耗时间而已
long long Fac(int n)
{
	 if(0 == n)
	 {
	 	return 1;
	 }
	 int i = 0;
	 for(i = 0; i < n; i++)
	 {
	 	printf("hello world!\n");
	 }
	 return Fac(n-1)*n;
}

分析:printf可以看成基本语句,递归n次,每次递归基本语句执行n次(注意:这里的n随着递归深度而减少,所以到最后一层递归这里的n就变成了0),所以基本语句的执行次数最终形成了一个等差数列:n、n-1、n-2 … 2、1、0,求和累加:n(n+1)/2,所以时间复杂度函数f(n) = 0.5n^2 + 0.5n,最终时间复杂度表示为:O(n^2)

第八题:计算Fib的时间复杂度

//求第N个斐波那契数列
long long Fib(int N)
{
	 if(N < 3)
	 {
		 return 1;
	 }
	 return Fib(N-1) + Fib(N-2);
}

分析:注意return语句后面两个递归调用,所以两个递归调用执行的基本语句的次数都要算,if或者return都可以看成基本语句,每次递归基本语句执行1次,那总共递归函数调用了多少次就是时间复杂度,调用多少次画一个图比较好计算:
在这里插入图片描述

从上图可以看出,第一层2^ 0次调用,第二层2^1次调用,第三层2 ^ 2次调用,第四层2^3次调用,依次类推就是一个等比数列求和,结果为:(2 ^(n-1)) - 1,时间复杂度函数f(n)=(2 ^(n-1)) - 1,化简为:f(n)=0.5(2^ n) -1,时间复杂度为O(2^n)

三、总结

1、时间复杂度是一种估算,通常以最坏情况为准
2、计算时间复杂度有时不方便从代码角度看的时候,应从算法的思想角度考虑会简单一点
3、注意计算递归函数的时间复杂度:多次调用的基本语句执行次数的累加

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值