数据结构 时间与空间复杂度就看这篇了【生活经历 + 实例讲解】_生活复杂度(1)

	- [🗡实例6~10讲解](#610_224)

⌚算法效率

📕如何衡量一个算法的好坏

  • 对于算法,相信大家在学习数据结构的时候就已经接触过不少了,就是对于一个问题的计算方法,那我们知道,对于一个问题的计算方法都多种多样的,有高效的计算方法,自然也有拙劣的计算方法,那我们怎么去判别一个算法的性能和效率呢?
  • 通过一段代码先来看看
  • 下面这段代码是通过递归去求解斐波那契数列。可能在我们的认知看来一个计算方法写的越多越厉害,也就效率越高;面对下面的这段代码,你可能会认为它是一个很简单的代码,复杂度并不是很高,但是却恰恰相反,它的时间复杂度为O(2N),呈现的是一个指数级别的增长

你想知道这是为什么吗?那就跟我来吧,带你真正学会计算时间与空间复杂度🖊

long long Fib(int N)
{
	if(N < 3)
		return 1;
	return Fib(N-1) + Fib(N-2);
}

📕算法的复杂度

  • 上面这段代码我们讲到了其时间复杂度为O(2N),其实比它复杂度高的算法还有很多,但比较的不仅仅是时间复杂度,还有空间复杂度,下面给出这两个复杂度的定义

👉【时间复杂度】主要衡量一个算法的运行快慢

👉【空间复杂度】主要衡量一个算法运行所需要的额外空间

  • 在早些年计算机还不是很发达的时候,计算机内部的存储容量是很小的,只有几K,甚至几个字节,可不会像我们现在的硬盘这么大的容量,所以那时候写一个算法对于空间复杂度还是会有所考虑。但是经过这些年计算机行业的飞速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度
  • 因此对于一个算法,我们更加注重的是时间复杂度,而不会刻意地去计算空间复杂度,一般这个空间复杂度的大小都是O(1),大一些的话也就O(N),不会特别地大到哪里去

📕复杂度在校招中的考察

  • 对于数据结构,那在校招中是很重要的一门课程,考点也会涉及的比较多,下面有一篇面试腾讯的学长所描述的面试经过,以及算法题的考核,可以看到,都会涉及到复杂度的知识,所以复杂度这一块是学习数据结构与算法的核心,也是大家打好数据结构基础最重要的一块,接下去让我们正式地进入复杂度的学习

在这里插入图片描述

⌚时间复杂度

🌳概述

首先我们要来说到的是时间复杂度

  • 时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们需要每个算法都上机测试吗?
  • 是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。

即:找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度

🌳如何表示时间复杂度?【大O的渐进表示法】

  • 了解了时间复杂度的基本概念后,我们需要学习如何去表示一个算法的时间复杂度,这就要使用到一个表示法,叫做【大O的渐进表示法】。首先对于这个【O】,我们不要O,叫做大O
  • 大O符号(Big O notation):是用于描述函数渐进行为的数学符号
  • 比如说我们刚才对于这个斐波那契数列,讲到它的时间复杂度为2N,那使用这个大O渐进表示法就是O(2N),那对于一个常数级别的表示法,就是O(1),括号内是算法执行的次数

🌳时间复杂度的分类

刚才说到了时间复杂度如何去表示,而且说到了O(2N)和O(1),那对于这个时间复杂度,还有哪些其他类别呢?我们一起来看看

🐸常数阶O(1)

在这里插入图片描述

🐸对数阶O(log2N)

在这里插入图片描述

🐸线性阶O(N)

在这里插入图片描述

🐸线性对数阶O(Nlog2N)

在这里插入图片描述

🐸平方阶O(N2)

在这里插入图片描述

🐸立方阶O(N3)

在这里插入图片描述

🐸指数阶O(2N)

在这里插入图片描述

🐸乘方阶O(N!)

在这里插入图片描述

  • 上面就是所有的时间复杂度分类,那它们之间是谁比谁复杂呢?其实根据我一张张画下来的顺序,就可以知晓了👇

O(1) < O(log2N) < O(N) < O(Nlog2N) < O(N2) < O(N3) < O(2N) < O(N!)

🌳推导大O方法【五条重要法则!!!】

知道了有哪些时间复杂度的类别,接下去你的任务就是先搞清楚规则,如何计算得到这些时间复杂度

  • 下面罗列了五条规则,这是你一定要牢记的!!!

📚【法则一】:用常数1取代运行时间中的所有加法常数
📚【法则二】:在修改后的运行次数函数中,只保留最高阶项
📚【法则三】:如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶
📚【法则四】:随着输入规模的增加,算法的常数操作可忽略不计,与最高次相乘的常数可以忽略
📚【法则五】:最高次项指数大的,随着N的增长,结果也会变快,N的最高次幂越小,算法效率越高

下面在讲解题目的时候会一直用到这些法则,如果翻来翻去不方便可以将其使用QQ截图钉在桌面上

🌳最坏、最好与平均

从这个标题可以看出,我们要了解的是一个算法的最好、最坏以及平均的时间复杂度

  • 首先给出它们三个的定义👇
    平均情况:任意输入规模的期望运行次数
    最坏情况:任意输入规模的最大运行次数(上界)
    最好情况:任意输入规模的最小运行次数(下界)
  • 然后我们来看一个实例。在一个长度为N的数组中寻找一个执行的,比如是在1~10这10个数中去找
  • 首先如果我们要找1,那直接第一个就是
  • 其次如果我们要找5,那需要遍历一半的数字
  • 最后如果我们要找10,那需要将整个数组遍历一遍才可以找到这个数

在这里插入图片描述

  • 在时间复杂度层面,这个1指的就是最好情况,10是最坏情况,而5呢,则是平均情况,这么说你对这三者应该是有点概念了

但可能还是有同学不太清楚,这里给出两个生活小案例,你一定可以理解

❤生活小案例一:和女朋友约会
  • 好,我们来说一下这个案例,假如有一天呢,你女朋友要约你出去吃饭,大概是想要约在下午四五点这样,但是呢你下午有事情要忙,可能可以忙完,可能刚好要忙到五点,这个时候女朋友要等你的答复,该怎么办呢?
  • 这个时候你是要和她约四点、四点半还是五点呢,记住,一定要约五点。你要想万一你工作就已经忙到四点五十了,然后要穿件西装洗把脸整理一下,然后前往等待地点的时候是你女朋友在哪里等你,那她会怎么想呢
  • 所以说若是你约到五点,但是你提前四点半就忙完了,然后穿着得非常帅气叼只玫瑰🌹在你女朋友楼下等她,那她只会觉得你很守时,而且提前到了,所以要以最坏的来

对了,醒醒,你没有女朋友🙅【doge】

💻生活小案例二:和老板汇报工作
  • 再来说一个生活中的实际小案例。例如说这段时间你老板给你派了一个任务,让你通过代码去实现一段业务逻辑,过几天他来验收,那这个时候呢你打开Visible Studio开始了你的操作,但是呢这段代码虽然思路很清晰,写起来却不是那么好写,因此在最后完成后你也没有把握说这段逻辑一定是正确的,百分之百不会有差错
  • 到了验收的日子,你的老板来问你要代码,然后顺便问你这段代码的稳定性怎么样,会不会出现问题,那你怎么回答呢?若是你回答不会有问题,肯定你的老板当场一定会疯狂夸奖你,但是呢到了程序真正到用户手中的时候,却漏洞百出,用户的体验感很不好,那这个时候老板就要找你喝茶了🍵
  • 可当你在验收的时候说【这段程序的代码逻辑不太好实现,日后可能需要改进,此时若是给用户使用的话可能会出现一些Bug】,那你的老板当时虽然会觉得你不太行,但是你道出了这个问题的本质,后期再慢慢留时间去修复就好了,总比炒鱿鱼好吧🦑

从上面两个生活小实例可以看出,我们需要考虑最坏的情况,留出一定的差错空间,这样容错率就可以大大降低

🌳实战演练【详细解说,最重要的部分】

接下去我将会通过10余个有关时间复杂度的计算案例,来帮助你去真正理解如何计算时间复杂度✍

🗡实例1~5讲解

实例一

// 计算Func1的时间复杂度?
void Func1(int N)
{
 	int count = 0;
	 for (int k = 0; k < 2 \* N ; ++ k)
	 {
	 	++count;
	 }
 	int M = 10;
	 while (M--)
	 {
		 ++count;
	 }
 	printf("%d\n", count);
}

  • 好,我带大家一步步来分析一下,首先你记住,要算时间复杂度,绝对不能仅仅看有几个循环,那时间复杂度就是几,这是一个大家的通病,在开头就提出来👈
  • 上面这个程序,首先看第一个循环,从0循环到2N,也就是循环了2N次,也意味着这个count++了2N次。然后对于下面的while循环,因为【M = 10】,因此M--便循环了10次,所以这段程序的总体时间复杂度就是他们之和也就是【2N + 10】
  • 好,我们来简化一下,首先根据法则一,可以把后面的10看作成为1,然后根据法则三,可以去掉N前面的2,所以,最后可以得出这个程序的总体时间复杂度为O(N)

实例二

  • 好,然后看第二个,代码稍微多了一点,但其实和上面一般是一样的,只是在上面加了两层for循环嵌套罢了
// 请计算一下Func1中++count语句总共执行了多少次?
void Func2(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;
	}
}

  • 首先来看外层循环,执行了N次,对于每一次的外层执行,都对应着N次的内层循环执行,因此其复杂度就是O(N2),下面的我们实例一算过了,为2N + 10,因此总体的时间复杂度为【N2 + 2N + 10】
  • 我们继续来通过法则简化一下,首先根据法则一,将10改为1,然后根据法则四,将1去除,接着根据法则二,只保留高阶项,对于谁高谁低,我在前面已然说过,若是遗忘,往上翻阅即可,最后可以看到,只剩下N2,前面并没有任何系数,所以不需要化简,最后可以得出这个程序的总体时间复杂度为O(N2)

实例三

  • 然后来看第三个
// 计算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);
}

  • 很明显,两个循环,第一个循环的执行次数是M次,第二个循环的执行次数是N次,所以将它们加起来即可,就是O(M + N),这里若是M == N,则可表示为O(2M)然后转化为O(M),但是这里注明了两个变量的大小是不同的,所以整体时间复杂度为O(M + N),无需化简

实例四

  • 这题很简单,送分,循环执行了100次,因此时间复杂度为O(100),根据法则一,可将这个100化为1,因此最终的时间复杂度是一个常数阶O(1)
// 计算Func4的时间复杂度?
void Func4(int N)
{
	 int count = 0;
	 for (int k = 0; k < 100; ++ k)
	 {
	 	++count;
	 }
	 printf("%d\n", count);
}

实例五

// 计算strchr的时间复杂度?
const char \* strchr ( const char \* str, int character );

  • 这个函数名是C语言里面的一个字符串操作函数,带你们看看
  • 根据描述可以知道,是查找在字符串中出现的第一个字符

在这里插入图片描述

  • 可以看到,这个算法的时间复杂度其实和在一个数组中查找一个数是一样的,但是我们还是要去取最坏的情况,也就是O(N),设想你取的是平均的时间复杂度O(N/2),但是需要查找的这个数再后面的一部分的时候,会超过O(N/2)了,此时就显得不精准
    在这里插入图片描述
📰生活哲理闲谈

例子讲了一大半了,不知道你对时间复杂度的计算有没有一个基本的掌握了,可能有些同学对这个法则还不太熟悉,那你要现将这个法则仔细的研读一遍,思考一下再去做题🤔

  • 其实对于这个时间复杂度呢,它本质上算的并不是时间,而是一个执行的结果次数,法则中我们有提到只保留这个高阶项,但是为什么只保留高阶项呢,这个其实你还是要去联想我们生活中的例子
  1. 假设你是一个富豪,已经坐拥百万的家产,每天只需要轻松玩乐即可,那这个时候有三笔生意同时与你又进行了签订,一个是10亿,一个是1千万,另一个是10万,那你会看重哪个呢?不用说,一定是那个10亿,因为你已经足够有钱,不会去看重那些小财富,而是只会盯紧金额最大的那一部分
  2. 近些年科学家发现了在银河系外有着一些奇特的星系,有些离我们10光年,有些呢则是离我们几百光年,但是这对你来说区别又在哪儿呢,无论是10光年还是几百光年之外的星系,距离你都是非常遥远的距离

🗡实例6~10讲解

好,经过一些闲谈后我们继续来看实例,相信你对时间复杂度的求解有了一个层面的理解

实例六

  • 第六个,就是大家最熟悉也是最用的最多的一种排序算法——冒泡排序,如果想看其他排序的可以看看我的这篇文章 链接
  • 大家可能完全性地认为其时间复杂度为O(N2),那你觉得这是它的最好、最坏还是平均呢?我们一起来分析一下
// 计算BubbleSort的时间复杂度?
void BubbleSort(int\* a, int n)
{
	//[0,n - 1)
	for (int i = 0; i < n - 1; ++i)
	{
		int changed = 0;
		for (int j = 0; j < n - 1 - i; ++j)
		{
			if (a[j] > a[j + 1])
			{
				swap(&a[j], &a[j + 1]);
				changed = 1;
			}
		}
		if (changed == 0)
			break;
		//PrintArray(a, n);
	}	
}

在这里插入图片描述

  • 好,我们抛开上面的代码来分析一下,因为大家可能看到两层for循环就本能地认为这是一个O(N2)的时间复杂度
  • 通过上面的图我们可以看出,有N个数字,那这个外层就会执行N - 1次,对于每一次的循环,会进行一个内部的比较,通过两两之间的不断比较,将这个最大或者最小的数字冒到最后面,这其实就是冒泡排序的本质所在,然后对于第一次的比较会比较N - 1次,第二次呢会比较N - 2次,第三次会比较N - 3次。。。依次类推,到了最后只会比较1次
  • 我们看时间复杂度看到不是有几层循环,而是看这个程序它运行了多少次,上面我们分析到,这个每次的比较次数是【N - 1】到【N - 2】。。。最后到【1】,因此可以看出这是呈现一个等差数列的递减形式可以使用等差数列的求和公式去计算总共比较了多少次,(N - 1)*N/2,最后根据我们的法则二、法则四,便可以得出最终的时间复杂度为O(N2)
  • 冒泡排序的时间复杂度是这么算出来的,你明白了吗❓

实例七

  • 实例七是关于二分查找的例子,有关二分查找相信大家都是蛮熟悉,是一种比较高效的查找算法,但是你知道它的时间复杂度是多少吗?我们一起来分析一下
// 计算BinarySearch的时间复杂度?
int BinarySearch(int\* a, int n, int x)
{
	 assert(a);
	 int begin = 0;
	 int end = n-1;
	 // [begin, end]:begin和end是左闭右闭区间,因此有=号
	 while (begin <= end)
	 {
		 int mid = begin + ((end-begin)>>1);
		 if (a[mid] < x)
		 	begin = mid+1;
		 else if (a[mid] > x)
		 	end = mid-1;
		 else
		 	return mid;
	 }
	 return -1;
}

  • 首先我们再来简述一下它的原理,通过两端的边界,每次取寻找这个中间值,然后去比较待查找数字和中间值的关系,以此来确定待查数字在给定数组的待查数据范围,所以二分法每次范围都是以一半一半缩小的,大概是下面这样👇

在这里插入图片描述

  • 我们需要求的是使用这个二分法去查找这个数时所需要的次数👈,这点首先你要明确
  • 然后根据二分法的特性,每次都是以一半一半呈现一个折半的查找,然后直到总的数字个数被除到1为止,从我上面的式子可以看到,N是题目给出的数字长度个数,最后的结果我们知道会走到1,那唯一不知道的就是到底除了几个2,也就是这个2上的指数是多少
  • 我们将其设置为x,然后你自己在草稿纸上列一个式子,就可以求出这个X的表达式,是log以2位底N的对数,然后你去联想上面我所给出的O(log2N)的时间复杂度的图,就可以很明确的知道二分法的时间复杂度是多少了
  • 对于这个log以2为底的这个2,其实在很多资料书籍上都会将其省去,因为这个对总体不存在影响,所以会简写成logN,但是最好不要再简化写成lgN,不太建议写成这样,容易产生误会

对于这个二分法,其实它是蛮高的,对比一般的暴力查找,可谓是天壤之别,我们通过一些数据来对比一下

在这里插入图片描述

  • 可以看到,随着数据量的增加,暴力查找需要的100W次、10亿次,但是对二分查找来说只需要10次20次就够了,因为从logN这个图像其实可以看出,越到后面它的坡度越加平缓,数据的爬升量也是慢慢地增加而已
  • 所以二分法其实还是挺牛逼的。但是为什么有些地方在查找数据的时候不使用二分查找,而是使用效率更高的哈希表或是红黑树呢,因为二分查找它是有一个条件限制的,需要在待查找元素已经有序的情况下才可以进行一个折半查找,所以就一定要对无序的数组进行一个排序
  • 但是我们知道,对于排序的话,也是需要时间复杂度的,例如说直接使用个快排也是要O(NlogN)的时间复杂度,所以对于二分法有着一定的容错率,因此在某些场合下就会使用其他的搜索算法

在这里插入图片描述
实例八

  • 接着我们来讲的是一个阶乘递归的时间复杂度计算。对于递归,其实我们不仅仅要考虑的是本次的执行次数,而且还要考虑递归的次数,所以这里先给出求解递归算法时间复杂度的定义

递归时间复杂度计算方法和技巧 —— 每次递归调用的执行次数累加

// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{
	 if(1 == N)
	 	return 1;
	 return Fac(N-1)\*N;
	 //算的是每次里面的执行可以是常数次
	//每次是常数次,调用了N次,N个1相加
}

  • 可以看到,对于时间复杂度,计算的还是执行的次数,但是对于递归呢,是一个累加的效果,因为既然是递归了,次数也是需要计算进去
  • 就像上面这题对于一次递归,内部只有一个if条件分支判断,因此单次的时间复杂度为O(1),是一个常量级别的。然后递归调用了N次,所以我们进行一个常量级别的求和接口也就是N个1相加,那也就是N
  • 因此这个算法的时间复杂度为O(N)

可能还有同学没看懂,我通过一张图给你看看。可以看到,每一次递归的调用,执行次数都只有一次,那随着N次递归,就有N - 1次调用,我们去计算总的执行次数就行

在这里插入图片描述

实例九

  • 然后我们再来看一个,是上面一个案例的改进版

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

行一个常量级别的求和接口也就是N个1相加,那也就是N

  • 因此这个算法的时间复杂度为O(N)

可能还有同学没看懂,我通过一张图给你看看。可以看到,每一次递归的调用,执行次数都只有一次,那随着N次递归,就有N - 1次调用,我们去计算总的执行次数就行

在这里插入图片描述

实例九

  • 然后我们再来看一个,是上面一个案例的改进版

[外链图片转存中…(img-PIgA5urr-1714802136933)]
[外链图片转存中…(img-S98bBmxG-1714802136934)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值