时间复杂度都去哪了?

1. 算法的好坏

时间复杂度和空间复杂度究竟是什么呢?首先,让我们来想象一个场景。

某一天,小灰和大黄同时加入了同一家公司。

一天后,小灰和大黄交付了各自的代码,两人的代码实现的功能差
不多。

大黄的代码运行一次要花100ms ,占用内存5MB 。

小灰的代码运行一次要花100s ,占用内存500MB 。

于是……

在上述场景中,小灰虽然也按照老板的要求实现了功能,但他的代码存在两个很严重的问题。

  1. 运行时间

运行别人的代码只要100ms,而运行小灰的代码则要100s,使用者肯定是无法忍受的。

  1. 占用空间大

别人的代码只消耗5MB的内存,而小灰的代码却要消耗500MB的内存,这会给使用者造成很多麻烦。

由此可见,运行时间的长短和占用内存空间的大小,是衡量程序好坏的重要因素。

总的来说:
①算法首先必须要准确无误
②时间复杂度和空间复杂度越小越好
③算法要是友好的,不装B,让他人能看得懂

2. what is the 时间复杂度?

在计算机里,他是描述一个算法的运行时间。
又∵花费的时间∝语句的执行次数
∴又转化为算法的基本操作的执行次数,作为算法的时间复杂度

3. 时间复杂度的意义

那肯定很多小白就觉得,为什么要引入这样一个麻烦的概念呢,直接用代码的运行时间作为代码的效率,不香吗?

但在实际过程中,由于编程的各个语言以及运行过程中的许多细节 (比如你的电脑卡了) 的影响,在运行过程中,是无法估量的,而且每次的运行时间都会有细小的偏差,所以引入时间复杂度来方便我们更好地估算程序的运行时间。

如果懒也可以这样

4. 基本操作执行次数

关于代码的基本操作执行次数,下面用生活中的4个场景来进行说明。

场景1

给小灰1个长度为10cm的面包,小灰每3分钟吃掉1cm,那么吃掉整个面包需要多久?

答案自然是3×10即30分钟。

如果面包的长度是n cm呢?

此时吃掉整个面包,需要3乘以n即3n分钟。

如果用一个函数来表达吃掉整个面包所需要的时间,可以记作T(n) = 3n (n为面包的长度)

场景2

给小灰1个长度为16cm的面包,小灰每5分钟吃掉面包剩余长度的一半,即第5分钟吃掉8cm,第10分钟吃掉4cm,第15分钟吃掉2cm……那么小灰把面包吃得只剩1cm,需要多久呢?

这个问题用数学方式表达就是,数字16不断地除以2,那么除几次以后的结果等于1?这里涉及数学中的对数,即以2为底16的对数log16(信息里的log16<=>数学里的log216)。

因此,把面包吃得只剩下1cm,需要5×log16即20分钟。

如果面包的长度是n cm呢?

此时,需要5乘以logn即5logn分钟,记作T(n) = 5logn 。(注:由于每次吃掉一半,就越吃越少由2T(n)/5=n得T(n)/5=logn,即T(n)=5 * logn)。

场景3

给小灰1个长度为10cm的面包和1个鸡腿,小灰每2分钟吃掉1个鸡腿。那么小灰吃掉整个鸡腿需要多久呢?

答案自然是2分钟。因为这里只要求吃掉鸡腿,和10cm的面包没有关系。

如果面包的长度是n cm呢?

无论面包多长,吃掉鸡腿的时间都是2分钟,记作T(n) = 2 。

场景4

给小灰1个长度为10cm的面包,小灰吃掉第1个1cm需要1分钟时间,吃掉第2个1cm需要2分钟时间,吃掉第3个1cm需要3分钟时间……每吃1cm所花的时间就比吃上一个1cm多用1分钟。那么小灰吃掉整个面包需要多久呢?

根据高斯算法,此时吃掉整个面包需要 1+2+3+…+(n-1)+ n 即(1+n)×n/2分钟,也就是0.5n2 + 0.5n分钟,记作T(n) = 0.5n2 + 0.5n 。
怎么除了吃还是吃啊?这还不得撑死?

上面所讲的是吃东西所花费的时间,这一思想同样适用于对程序基本操作执行次数 的统计。设T(n)为程序基本操作执行次数的函数(也可以认为是程序的相对执行时间函数),n为输入规模,刚才的4个场景分别对应了程序中最常见的4种执行方式。

场景1  T(n) = 3n,执行次数是线性的。

inline void eat1(int n){
	for(int i = 1; i <= n; i++){
		printf("wait for 1 min");
		printf("wait for 1 min");
		printf("eat 1cm long of the bread");
	}
}

场景2  T(n) = 5logn,执行次数是用对数计算的。

inline void eat2(int n){
	for(int i = n; i > 1; i >>= 1){//>>1 <=> /2
		printf("wait for 1 min");
		printf("wait for 1 min");
		printf("wait for 1 min");
		printf("wait for 1 min");
		printf("eat half of the bread")
	}
}

场景3  T(n) = 2,执行次数是常量。

inline void eat3(int n){
	printf("wait for 1 min");
	printf("Eat a drumstick");//drumstick 鸡腿🍗 英[ˈdrʌmstɪk] 美[ˈdrʌmstɪk]
}

场景4  T(n) = 0.5n2 + 0.5n,执行次数是用多项式计算的。

inline void eat4(int n){
	for(int i = 1; i <= n; i++){
		for(int j = 1; j <= i; j++)
			printf("wait for 1 min");
		printf("eat 1cm long of the bread");
	}
}

5. 渐进时间复杂度

有了基本操作执行次数的函数T(n),是否就可以分析和比较代码的运行时间了呢?还是有一定困难的。

例如算法A的执行次数是T(n)= 100n,算法B的执行次数是T(n)= 5n2,这两个到底谁的运行时间更长一些呢?这就要看n的取值了。

若存在函数f(n),使得当n->∞时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称为O(f(n)。(O为算法的渐进时间复杂度,简称为时间复杂度)

因为渐进时间复杂度用大写O来表示,所以也被称为大o表示法(O渐进表示法) 。

这个定义好晦涩呀,看不明白。

直白地讲,时间复杂度就是把程序的相对执行时间函数T(n)简化为一个数量级,这个数量级可以是n、n2 、n3等。

如何推导出时间复杂度呢?有如下几个原则。

  • 如果运行时间是常数量级,则用常数1表示
  • 只保留时间函数中的最高阶项
  • 如果最高阶项存在,则省去最高阶项前面的系数

让我们回头看看刚才的4个场景。

场景1
T(n) = 3n

最高阶项为3n,省去系数3,则转化的时间复杂度为:T(n)=O(n)。(线性阶)

场景2

T(n) = 5logn

最高阶项为5logn,省去系数5,则转化的时间复杂度为:T(n)=O(logn) 。(对数阶)

场景3

T(n) = 2

只有常数量级,则转化的时间复杂度为:T(n) =O(1) 。(常数阶)

场景4

最高阶项为0.5n2,省去系数0.5,则转化的时间复杂度为:T(n)=O(n 2 ) 。(平方阶)

inline void eat4(int n){
	for(int i = 1; i <= n; i++){
		for(int j = 1; j <= i; j++)}
}

我们可以模拟一下:
当i=1时,内循环执行了1次,当i=1时,执行了2次,……当i=n时,执行了n次。所以总的执行次数为:

1 + … + (n - 2) + (n - 1) + n = n(n + 1) / 2 = n2 / 2 + n / 2
然后再用大o表示法,
①只保留时间函数中的最高阶项——n2 / 2;
②如果最高阶项存在,则省去最高阶项前面的系数——省去系数1 / 2;
=>时间复杂度为O(n2)

这4种时间复杂度究竟谁的程度执行用时更长,谁更节省时间呢?当n的取值足够大时,不难得出下面的结论:
在这里插入图片描述
在编程的世界中有各种各样的算法,除了上述4个场景,还有许多不同形式的时间复杂度,eg.
在这里插入图片描述

⚠补充一下其他一些常用的时间复杂度:

O(nlogn)算法 (线性对数阶)

for(int i = 1; i <= n; i++)//n
	for(int j = n; j > 1; i >>= 1)//logn

=>n * logn

O(n3)算法 (立方阶)

	for(int i = 1; i <= n; i++)//n
		for(int i = 1; i <= n; i++)//n
			for(int i = 1; i <= n; i++)//n}

还可以推广成O(nk)(k次方阶)

O(2n)算法 (指数阶)

for(int i = 1; i <= pow(2, n); i++){}

O(n!) (阶乘阶)

inline void f(int n){
	if(!n) return;//!n <=> n == 0
	for(int i = 1; i <= n; i++) f(n - 1);
}

常见的时问复杂度如表所示。

算法执行次数的函数大O阶非正式术语
12O(1)常数阶
2n+3O(n)线性阶
3n2+2n+1O(n2)平方阶
5logn+20O(logn)对数阶
2n+3nlogn+19O(nlogn)线性对数阶
6n3+2n2+3n+4O(n3)立方阶
2nO(2n)指数阶

常用的时间复杂度所耗费的时间从小到大依次是:

O(1)<O(logn)<O(n)<O(nlogn)<O(n^2)<O(n^3)<O(2^n)<O(n!)<O(n^n)

今后当我们遨游在代码的海洋中时,会陆续遇到上述时间复杂度的算法。

6. 时间复杂度的巨大差异

举例如下。

算法A的执行次数是T(n)= 100n,时间复杂度是O(n)。

算法B的执行次数是T(n)= 5n2 ,时间复杂度是O(n2)。

算法A运行在小灰家里的老旧电脑上,算法B运行在某台超级计算机上,超级计算机的运行速度是老旧电脑的100倍。

那么,随着输入规模n的增长,两种算法谁运行速度更快呢?

从上面的表格可以看出,当n的值很小时,算法A的运行用时要远大于算法B;当n的值在1000左右时,算法A和算法B的运行时间已经比较接近;随着n的值越来越大,甚至达到十万、百万时,算法A的优势开始显现出来,算法B的运行速度则越来越慢,差距越来越明显。

这就是不同时间复杂度带来的差距。

7. 平均与好坏

在这里插入图片描述
你肯定在网上或算法书上曾看过某些算法的复杂度,它会有一栏是最坏的情况,还有一栏平均的情况。下面,我将教你如何计算这些数据。

So, how to calculate the data?

8. 计算规则

计算规则
看不懂就听天由命吧

在这里插入图片描述好啦,最后再说一句:要想学好算法,就必须理解时间复杂度这个重要的基础概念。

9. 题目

例题1
在这里插入图片描述
例题2
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值