数据结构(2.1)——时间复杂度和空间复杂度计算

2023-11-12

前言

(1)因为上一篇博客:数据结构(2)—算法对于时间复杂度和空间复杂度计算的讲解太少。所以我在次增加多个案例讲解。
(2)上一篇已经详细介绍了,为什么我们的算法要使用复杂度这一个概念。因此,我这一篇将重点介绍,复杂度如何进行计算。

时间复杂度计算

(1)上一篇博客已经介绍了,一般情况下,我们是不会考虑空间复杂度的。所以我将会重点介绍时间复杂度的计算。
(2)我们之前说了,时间复杂度是采用的大O计法。(注:不懂的建议看上一篇的算法的时间复杂度部分,我已经介绍的很详细了)
(3)接下来我将直接上代码进行计算。我先贴代码,然后再讲解。如果感兴趣的,可以看代码自己先计算一边,然后再看解析。

示例1—入门训练

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;
	}
	printf("%d\n", count);
}

(1)根据大O计法,我们知道,先要找到这个函数的具体执行时间。
(2)首先,我们看到两个for语句嵌套,第一个for语句里面需要执行N次,而第二个for语句嵌套在里面,所以最终的执行次数为N^2次。

	for (int i = 0; i < N ; ++ i)  //执行N次
	{
		for (int j = 0; j < N ; ++ j)  //执行N*N次,所以最终结果是执行了N^2次
		{
			++count;
		}
	}

(3)第二个for语句里面没有嵌套,条件判断为 <2*N,而且每次自增为1。所以这里需要执行2N次。

	for (int k = 0; k < 2 * N ; ++ k)
	{
		++count;
	}

(4)最后一个while中执行M次,M给定了一个常量10。所以这个while是执行10次。

	int M = 10;
	while (M--)
	{
		++count;
	}

(5)综上所述,最终得出的结论是,这个代码执行次数如下
T ( n ) = n 2 + 2 n + 10 T(n)=n^{2} + 2n +10 Tn=n2+2n+10
(6)而根据大O计法,可知,当f(n)为n^2。n趋向于无穷大,最终T(n)/f(n)为常数。所以时间复杂度为O(n ^ 2)。
(7)总结,大O计法就是保留代码执行字数的最高项。

示例2—最高项的常数不唯一

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);
}

(1)同理,先计算出这个函数总体执行次数。
(2)第一个for循环,判断条件为 k < 2 * N,K每次自增为1,所以需要执行2N次。

	for (int k = 0; k < 2 * N ; ++ k)
	{
		++count;
	}

(3)同理,这个while固定执行10次。

	int M = 10;
	while (M--)
	{
		++count;
	}

(4)所以,这个函数最终执行的次数为 2N+10。那么根据大O计法,这个时间复杂度难道是O(2N)?
(5)看到我这么说,那么答案肯定不对。大O计法规定了 ,如果最高项的常数不是1,则需要去除最高想的常数。比如,此题的最高项为N,他的常数为2,所以这一题的时间复杂度为O(N)。

示例3—出现多个变量

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);
}

(1)这个题目,我们会发现有两个变量,那么时间复杂度怎么进行计算呢?其实也没有想象的那么复杂,只需要按情况分析即可。
(2)第一,假设N和M差不多大小,那么时间复杂度就是O(N+M)。
(3)第二,假设N远大于M,那么时间复杂度就是O(N)。
(4)第二,假设N远小于M,那么时间复杂度就是O(M)。

示例4—执行次数唯一

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

(1)我们看这个函数,会发现,传入形参N是没有被使用到的。也就是说,这个函数固定运行100次。
(2)对于这种情况,可能有些人就有点懵了。那么他的时间复杂度是多少呢?
(3)大O计法规定了,如果是固定了执行次数的函数。时间复杂度为O(1)。

示例5—执行次数存在多种情况

const char * strchr ( const char * str, int character )
{
	while(*str != '\0')
	{
		if(*str == character)
		{
			return str;
		}
		str++;
	}
	return NULL;
}

(1)首先说明一下这个函数的作用。假设我们有一个字符串"sdyzscx",我要找到这个字符串的字符’y’,那么他将会遍历这个字符串。如果找到了字符’y’,将会返回该字符的位置。否则返回一个空指针。
(2)这个题目,我们会发现,很难找到他的具体运行时间。因为假设我们要寻找的字符永远是字符串的第一个,那么这个函数的执行字数永远是1,那么时间复杂度就是O(1)。我们将这种情况称之为,最好情况。
(3)但是假如我们每次要寻找的字符总是出现在中间位置,也就是只要执行N/2次。这种叫做平均情况。
(4)最后一种,就是我们保持绝对的悲观态度,假设我们寻找的字符永远是字符串最后一个字符,或者找不到。那么执行次数为N,时间复杂度为O(n)。这种叫做最坏情况。
(5)在实际中,一般我们都是关注的最坏运行情况,所以此题的时间复杂度为O(n)。

示例6—执行次数不直观

void BubbleSort(int* a, int n)
{
	assert(a);
	for (size_t end = n; end > 0; --end)
	{
		int exchange = 0;
		for (size_t i = 1; i < end; ++i)
		{
			if (a[i-1] > a[i])
			{
				Swap(&a[i-1], &a[i]);
				exchange = 1;
			}
		}
		if (exchange == 0)
			break;
	}
}

(1)这就是一个冒泡排序算法,但是我们会发现,这个题目的似乎运行次数不太好进行计算。这个时候,最好的方式是带入具体数值。
(2)假设n为10,传入的数组元素有10个。
<1>第一次运行,第一个for语句进入判断,第二个for语句的判断end就是10。所以这里是执行9次。
<2>第二次运行,这个时候,end为9了。那么第二个for语句就是执行8次。
<3>依次类推,我们可以知道,这里就是执行了9!(注意’!'表示阶乘的意思,不明白的可以看看高中课本)。
<4>如果将具体数字转换为变量n,就可以得出,这个函数实际上执行次数为:(n-1)! 根据小学的知识,我们可以知道,执行次数为
( n − 1 + 1 ) ( n − 1 ) ) 2 = n 2 − n 2 \frac{(n-1+1)(n-1))}{2}=\frac{n^{2}-n}{2} 2(n1+1)(n1))=2n2n
<5>根据大O计法可知,最终的时间复杂度为O(n^2)

示例7—二分查找的时间复杂度

/* 作用:二分查找,用于寻找有序数组的值
 * 传入参数 : 
     * a : 有序数组的首元素地址
     * n : 该元素的长度
     * x : 要查找的元素
 * 返回值 : 如果找到了元素,返回该元素在数组的第几项。没有找到元素,返回-1。
*/
int BinarySearch(int* a, int n, int x)
{
	assert(a);
	int begin = 0;
	int end = n-1;
	while (begin < end)
	{
		int mid = begin + ((end-begin)>>1);
		if (a[mid] < x)
			begin = mid+1;
		else if (a[mid] > x)
			end = mid;
		else
			return mid;
	}
	return -1;
}

(1)这个函数就是一个有序数列的二分查找函数。
(2)看到这个函数,依旧是没有任何思路的,所以还是直接套数字。假设有一个数列[0,1,2,3,4,5,6,7,8,9],因为计算时间复杂度是按照最坏的条件来进行计算,所以假设我们需要找到的数字为0。
<1>首先,传入这个数组,begin为0,end为9(注意,end为9,但是是指向的数组的最后一个元素,因为数组的第一个元素从0开始)。mid=(9-0)>>1 =4,所以min指向元素4。

在这里插入图片描述

<2>我们会发现0是小于4的,所以end=mid=4。mid=0+(4-0)>>1=2。

在这里插入图片描述

<2>这个时候,我们依旧会发现0是小于2的,所以end=mid=2。mid=0+(2-0)>>1=1。

在这里插入图片描述

<3>0是还是小于1的,所以end=mid=1。mid=0+(1-0)>>1=0。

在这里插入图片描述

<4>最后,我们会发现a[mid] == 0,值返回。
(3)
<1>根据上面这个例子,我们会发现,最坏情况需要运行4次。
<2>所以,我们假设一个有序数组有N项,由于我们按照最坏的情况进行讨论,所以每次寻找,就排除了1/2的数据。
<3>第一次寻找,还剩下N/2项,
<4>第二次寻找,还剩下N/4项。
<5>依次类推,我们假设最坏的情况是找了X次。那么最终满足条件2^(X-1)<N<2 ^X,那么X就找到了。
<6>因此,时间复杂度满足如下公式,但是很少部分时候,有些数据结构的书中,写成后面这个式子。(注意,与数学的写法不同!不严谨但是很多时候当成是等于)
O ( l o g 2 N ) = O ( l g N ) O({log_{2}}^{N}) = O(lg_{N}) O(log2N)=O(lgN)

示例8—递归调用函数的时间复杂度

long long Factorial(size_t N)
{
	return N < 2 ? N : Factorial(N-1)*N;
}

(1)这个依旧无法直接看出结果,依旧套具体数值来寻找思路。假设N为10。那么他的函数调用关系如下。
(2)我们会发现,传入的N为多少,那么执行的次数就是多少。所以时间复杂度为O(N)。

在这里插入图片描述

空间复杂度计算

示例1—初级训练

(1)看了上面这么多例子之后,我们对时间复杂度的计算应该还是比较了解了。那么如何计算空间复杂度呢?拿下面这个例子开始计算。
(2)空间复杂度也是采用的大O计法,首先我们先数一下下面这个函数中,建立了多少个变量。
(3)我们看到,这个函数中就只建立了一个变量exchange ,但是这个exchange 是在for循环里面的,那么这个函数的空间复杂度就是O(N)吗?

void BubbleSort(int* a, int n)
{
	assert(a);
	for (size_t end = n; end > 0; --end)
	{
		int exchange = 0;
		for (size_t i = 1; i < end; ++i)
		{
			if (a[i-1] > a[i])
			{
				Swap(&a[i-1], &a[i]);
				exchange = 1;
			}
		}
		if (exchange == 0)
			break;
	}
}

(4)答案是错误的,因为数据结构中,时间是累积的,空间是不累积的。可能有些人看到这句话,就蒙圈了。什么意思呢?
(5)首先我们得知道,exchange 这个变量虽然是在for循环中,但是每次for循环不会都创建一个exchange 变量,而是重复利用同一个空间。如下图,因此,这里的空间复杂度为O(1)。

在这里插入图片描述

示例2—递归调用的空间复杂度计算

long long Factorial(size_t N)
{
	return N < 2 ? N : Factorial(N-1)*N;
}

(1)根据上面的知识之后,那么这个函数的空间复杂度是多少呢?
(2)答案很简单,为O(N)。为什么呢?因为在递归调用的时候,每一次函数调用,都会保留上一次的值。
在这里插入图片描述

(3)而最终调用到Factorial(1)的时候,结束函数调用,开始返回,就开始销毁之前的变量。

在这里插入图片描述

示例3—malloc函数的空间复杂度计算

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 ;
}

(1)这是一个斐波那契数列,这个空间复杂度是多少呢?
(2)其实对于这种空间复杂度和时间复杂度计算,最重要的是看这个函数与传入参数有没有关系。如果没有关系,那么大概率是O(1)。有关系,就只需要看有关系的那一部分。
(3)我们会发现,这个函数,只有malloc函数与传入参数n有关系,所以其实我们需要看malloc那一句话。因为malloc申请到的数据为n+1,所以这个函数的空间复杂度就是O(N),其他地方根本不需要看。

总结

(1)时间复杂度和空间复杂度都是采用的大O计法。复杂度计算只需要看与函数传入参数有关系的部分
(2)时间复杂度要点:
<1>只需要看最高项。
<2>最高项的常数可以忽略。
<3>时间复杂度一般只看最坏情况。
(3)空间复杂度要点:
<1>时间是累积的,空间是不累积的。
<2>一般情况,我们只考虑时间复杂度,不考虑空间复杂度。
(4)根据复杂度,我们可以很好的衡量一个算法的优缺点,不同时间复杂度的执行时间由小到大。

在这里插入图片描述

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

数据结构(2.1)——时间复杂度和空间复杂度计算 的相关文章

随机推荐

  • PROFINET趣谈——设备模型

    设备名 MAC地址和IP地址是为了在网络中找到对应设备 而要定位确切的输入 IX1 1 输出 QW2 就需要熟悉设备模型的概念 PROFINET IO的设备类型与PROFIBUS几乎相同 如图所示 设备模型包括若干槽 slot 与子槽 su
  • Java内存泄露监控工具:JVM监控工具介绍

    jstack 如果java程序崩溃生成core文件 jstack工具可以用来获得core文件的java stack和native stack的信息 从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题 另外 jstack工具还可以附
  • BUAA词频统计(树实现)

    问题描述 编写程序统计一个英文文本文件中每个单词的出现次数 词频统计 并将统计结果按单词字典序输出到屏幕上 要求 程序应用二叉排序树 BST 来存储和统计读入的单词 注 在此单词为仅由字母组成的字符序列 包含大写字母的单词应将大写字母转换为
  • Linux 解决vi键盘方向键出现字母的问题

    修改 etc vim vimrc tiny 1 将 set compatible 兼容模式 改成 set nocompatible 非兼容模式 2 添加 set backspace 2 解决退格键无法使用
  • 【完全开源】小安派-Cam-D 摄像头核心板

    文章目录 一 概述 二 系统框图 三 摄像头电路 四 内存卡电路 五 IO引脚说明 六 资料 一 概述 小安派 Cam D AiPi Cam D 是安信可科技为高性能模组Ai M61 32S设计的一款摄像头核心板 引脚完全兼容Ai WB1
  • MFC :CCoolBar 的替代方案 CDockablePane。

    阅读受众需有一定MFC知识储备 技术支持 http www cnblogs com shuhaoc archive 2011 06 26 cdockableform html 在以往很多使用CCoolBar实现窗口停靠功能 但是在VS201
  • 【C++】Modbus通讯

    C Modbus通讯 2016年06月22日 20 37 48 Taily老段 阅读数 10298 版权声明 本文为博主原创文章 未经博主允许不得转载 如遇到疑问 评论会给出答复 学习交流 关注页面微信公众号 吃良心 拉思想 https b
  • R语言入门教程知识 第七章 特殊值

    以下为本章所用代码 letters letters 5 9 LETTERS LETTERS 6 10 month name month name 7 11 month abb month abb 8 12 pi NA length vec
  • 手撕self-attention代码_从0实现self-attention_附学习路线

    一 前言 科研需要 前几天自学了transformer 在理解self attention时 发现网上并没有一套成熟易懂的学习路线 对新手及其不友好 大多数教程只重视理论和公式的讲解 没有从零开始的代码实战 因此 我在这里整理了一条最适合新
  • python爬虫实战之最简单的网页爬虫教程

    在我们日常上网浏览网页的时候 经常会看到一些好看的图片 我们就希望把这些图片保存下载 或者用户用来做桌面壁纸 或者用来做设计的素材 下面这篇文章就来给大家介绍了关于利用python实现最简单的网页爬虫的相关资料 前言 网络爬虫 又被称为网页
  • 十--nodejs原理(事件循环)

    一 事件循环 什么是事件循环 事件循环使得nodejs可以通过将操作转移到系统内核中来执行非阻塞I O操作 尽管javascripts是单线程的 由于大多数现代内核是多线程的 因此它们可以处理在后台执行的多个操作 当这些操作之一完成时 内核
  • 最短路问题(各种方法整理)附上一个完美模板

    最短路问题 short path problem 从某点出发到达另一点所经过的路径权值相加最小的一条路径 就是最短路径 经典的也是最容易掌握的方法Floyd Dijkstra两种算法 1 Floyd算法 Floyd算法可以求解的是任意两点的
  • 快速上手 ChatGPT 进行信息检索或代码构建 (最近爆火的对话语言模型)

    文章目录 上手使用几步骤 ChatGPT 是什么 ChatGPT 能做什么 给予算法和技术学习参考 进行通用事项细节了解 国际化搜索且经过优化 实战 ChatGPT 汽车概论的论文 python快速排序 什么是分治思想 注意这里产生了上下文
  • 某系统异常流量安全分析案例

    异常分析原理 正常的基于TCP的网络流量 都是先建立TCP连接 再传输数据 然后断开连接 而网络中经常存在中毒系统 配置错误等问题 导致网络中存在单向请求现象 这些信息也会在网络流量中体现 NetInside自动发现大量连接请求 但对方没有
  • YACC工具ParserGenerator的下载和配置过程

    工具准备 parser generator http www bumblebeesoftware com downloads htm VC6 0 网上到处都是 1 parser generator的环境设置 安装好parser genera
  • RedisTemplate存储对象乱码解决

    SpringBoot 通过RedisTemplate存储对象时 key和Value乱码 特此记录 解决方法 import org springframework beans factory annotation Autowired impo
  • 深入浅出PID控制算法(二)————PID算法离散化和增量式PID算法原理及Matlab实现

    引言 上篇介绍了连续系统的PID算法 但是计算机控制是一种采样控制 他只能根据采样时刻的偏差来计算控制量 因此计算机控制系统中 必须对公式进行离散化 具体就是用求和代替积分 用向后差分来代替微分 使模拟PID离散化为数字形式的差分方程 准备
  • Java动态代理一——动态类Proxy的使用

    1 什么是动态代理 答 动态代理可以提供对另一个对象的访问 同时隐藏实际对象的具体事实 代理一般会实现它所表示的实际对象的接口 代理可以访问实际对象 但是延迟实现实际对象的部分功能 实际对象实现系统的实际功能 代理对象对客户隐藏了实际对象
  • CSS选择器总结

    元素选择器 作用 通过元素选择器可以选择页面中的所有元素 语法 标签名 如下 选中所有的P标签 p color red font size 40px ID选择器 作用 通过元素ID属性值选中唯一的一个元素 语法 id属性值 如下 选中ID为
  • 数据结构(2.1)——时间复杂度和空间复杂度计算

    前言 1 因为上一篇博客 数据结构 2 算法对于时间复杂度和空间复杂度计算的讲解太少 所以我在次增加多个案例讲解 2 上一篇已经详细介绍了 为什么我们的算法要使用复杂度这一个概念 因此 我这一篇将重点介绍 复杂度如何进行计算 时间复杂度计算