[初阶数据结构】时间复杂度与空间复杂度

 

 前言

📚作者简介:爱编程的小马,正在学习C/C++,Linux及MySQL。

📚本文收录于初阶数据结构系列,本专栏主要是针对时间、空间复杂度,顺序表和链表、栈和队列、二叉树以及各类排序算法,持续更新!

📚相关专栏C++及Linux正在发展,敬请期待!

本文主要讲解时间复杂度以及空间复杂度,对大O渐进法会有比较详细的讲解,相信大家学完本文,会对复杂度有个很好的理解,那现在开始吧!

目录

 前言

1. 算法效率

1.1 如何衡量一个算法的好坏? 

1.2 算法的复杂度

2. 时间复杂度

2.1 时间复杂度的概念

2.2 大O的渐进表示法

 2.3 常见的时间复杂度计算举例

3. 空间复杂度 

4. 复杂度的OJ练习

4.1 消失的数字OJ链接:消失的数字 

4.2 轮转数组OJ链接 :轮转数组

总结


1. 算法效率

1.1 如何衡量一个算法的好坏? 

首先给大家看一段求斐波那契数列数:

int Feb(int n)
{if (n < 3)return 1;elsereturn Feb(n - 1) + Feb(n - 2);
}
int main()
{int n = 0;scanf("%d", &n);int ret = Feb(n);printf("%d\n", ret);return 0;
}

斐波那契实现递归非常的简洁,但是简洁的代码一定好吗?简洁的代码执行效率就高吗?其实不是的,我可以告诉大家,这个递归的时间复杂度是O(2^n),比如我想求第50个斐波那契数,是很慢的。那我们如何衡量一个代码是好还是坏呢?

1.2 算法的复杂度

算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。

时间复杂度主要衡量一个算法运行的快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。在计算机发展的早起,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的地步,所以我们如今已经不再需要特别关注一个算法的空间复杂度,而是要两者综合考虑。

2. 时间复杂度

2.1 时间复杂度的概念

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

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

例如:
 

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", count);
}

Func1执行的基本操作次数:

F(N) = N^{_{2}}+2*N+10 

怎么样得到的这个函数表达是呢?挨个循环分析,首先第一个 i 和 j 控制的循环,是不是里外都是N次,求和就是N*N ,其次第二个k控制的循环,是不是2N次,最后是M控制的循环,是10次,时间是累计的,所以用加法加在一起就是上文代码的时间复杂度。

 实际上,我们不需要精确到这种程度,因为没有意义。只是需要大概的精确度,可以知道这个算法的时间复杂度是什么level就可以了。那么这就需要我们使用大O的渐进表示法

2.2 大O的渐进表示法

 大O符号:是用于描述函数渐进行为的数学符号。

推导大O阶的方法:

1、用常数1取代运行时间中的所有加法常数

2、在修改后的运行次数函数中,只保留最高阶的函数

3、如果最高阶存在且不是1,则去除与这个项目想乘的常数,得到的结果就是大O阶。

上文函数算法如果使用大O阶的方法,那就是保留最高阶的函数,也就是时间复杂度为:               

F(N) = O(N)

另外有些算法的时间复杂度存在最好,平均和最坏三种情况

例如:在长度为N的数组中搜索X

最好的情况,是不是第一个就找到X了,这个时候1次找到

平均情况,在中间找到X,这个时候是N/2次找到

最坏情况,遍历完一次数组,N次找到

在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)

 2.3 常见的时间复杂度计算举例

实例1:

// 计算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);
}

计算Func2的时间复杂度,首先需要简单计算出数学表达式,K控制的循环有2N次,M控制的循环有10次,那时间复杂度函数就是:

F(N) = 2*N+10 

但是根据我们的大O表示法,F(N) = O(N) 

实例2:

// 计算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);
}

计算Func3的时间复杂度,F(N) = M+N ,这个时候都不能省略,因为M对复杂度的影响和N一样,所以该算法的时间复杂度是 F(N) = O(M+N)

实例3:

// 计算Func4的时间复杂度?
void Func4(int N)
{int count = 0;for (int k = 0; k < 100; ++k){++count;}printf("%d\n", count);
}

计算Func4的时间复杂度,F(N) = 100 ,根据大O渐进法,常数都要置为1,所以该算法的时间复杂度是   F(N) = O(1)

实例4:

const char* strchr(const char* str, int character);

计算这个函数的时间复杂度? 找一个字符串,可能1次就找到了,也可能找了一半找到了,也可能找到最后一个找到了,也可能找不到了,上文我们提到,要考虑最坏的情况,所以N次查找,该函数的时间复杂度也就是   F(N) = O(N)

实例5:

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

这是一个升序的冒泡排序,BubbleSort的时间复杂度是多少?

先简单给大家介绍一下,在冒泡排序里有详细讲解,就是两两对比,你比我大我就往后移动,直到比完,最大的就到后面去了。第一次需要比n-1 ,第二次需要比n-2 次,直到最后1次

我们用等差数列求和来算一下时间复杂度

F(N) = \frac{N*(N-1))}{2}

那么根据大O渐进法,F(N) = O(N^2)

实例6:

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;elsereturn mid;}return -1;
}

这是二分查找法,那么如何求时间复杂度呢?实际上是这样的,就是我每查找到一次,就除以2对不对,那我查找X次找到了,是不是可以有这个表达式:

2^{n} = x   

 实际上,F(N)=\log_{2}x

所以,时间复杂度就是 f(N) = O(\log_{2}N)

实例7:

long long Fac(size_t N)
{if (0 == N)return 1;return Fac(N - 1) * N;
}

这个Fac,我每调用一次函数,是不是都是常量个时间复杂度,那我调用了多少次函数?是N次,所以Fac的时间复杂度就是 F(n)= O(n)

实例8:

int Feb(int n)
{if (n < 3)return 1;elsereturn Feb(n - 1) + Feb(n - 2);
}
int main()
{int n = 0;scanf("%d", &n);int ret = Feb(n);printf("%d\n", ret);return 0;
}

上文给大家埋下伏笔的这个代码,应该是如何求时间复杂度,和实例7一样,就是看函数的调用,画个图给大家看看:

实际上是等比数列的求和,为什么可以先求左边的,而不管右边开辟的函数,因为函数栈帧是可以重复利用的,实际上调用完就销毁了,留个下一个函数使用。所以斐波那契数列的时间复杂度表达式是:

F(N) =2 ^{_{}^{N-1}}-1(并没有经过二叉树的复杂计算,就是个估计值,等以后写了二叉树系列在和大家讲解详细的算法)

所以,斐波那契数的时间复杂度是:

F(N)=2^{N} 

3. 空间复杂度 

空间复杂度也是一个数学表达式,是对一个算法在进行过程中临时占用存储空间大小的量度 。

空间复杂度不是程序占用了多少字节的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟时间复杂度类似,也是用大O渐进法。

注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器的信息等)在编译前已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。

实例1:

// 计算BubbleSort的空间复杂度?
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;}
}

如何计算呢?其实数组以及形式参数n已经是建立好的,不计入空间复杂度计算中。只有exchangde这个显式申请的额外空间需要计入。开辟了常数个额外空间,所以是O(1)

实例2:

// 计算Fibonacci的空间复杂度?
// 返回斐波那契数列的前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;
}

可以看到,动态内存开辟了一个数组,那么我们空间复杂度就是O(N)

实例3:

// 计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{if (N == 0)return 1;return Fac(N - 1) * N;
}

可以看到,这里的额外空间主要就是函数的调用,那么其实函数栈帧的开辟是可以重复使用的,所以,这里的空间复杂度就是O(N) 

4. 复杂度的OJ练习

4.1 消失的数字OJ链接:消失的数字 

题目是这样的,数组nums包含从0n的所有整数,但其中缺了一个。 题目要求,要在O(N)时间内完成。那么我们最好想的是什么办法?先给数组排个序,排序了之后,让数组看看是不是后一个等于前一个+1,如果不是,那么上一个+1就是小时的数字,那么这个时间复杂度是多少呢?其实是

F(N)=O(n*logn),题意,那我再介绍两个方法。

1、异或法

原理:我把原数组中所有的数字进行异或,再讲0-n的所有整数进行异或,最后输出的就是消失的数字,为什么会这样呢?因为相同的就全部异或为0了,而保留下来的就是消失的数字,那这个时间复杂度是多少呢,实际上是O(2N-1)也就是O(N),看代码:

int missingNumber(int* nums, int numsSize)
{int k = 0;for(int i = 0; i<numsSize;i++){k^= nums[i];}for(int i = 0; i<numsSize+1;i++){k^=i;}return k;
}

还有一种方法,对0-n的等差数列求和,求出来的值和缺失数字的数组挨个相减,即可得到消失的数字。看代码:

int missingNumber(int* nums, int numsSize)
{int sum = ((numsSize+1)*(numsSize))/2;for(int i = 0;i<numsSize;i++){sum -= nums[i];}return sum;
}

4.2 轮转数组OJ链接 :轮转数组

比方说 1 2 3 4 5 6 7 ,旋转一次 7 1 2 3 4 5 6 ,旋转两次 , 6 7 1 2 3 4 5,可以看到,我们有一个方法,是不是每次把最后一个拿出来,所有数组往后移动,再插入到第一个,这个的时间复杂度是O(N^2),很慢。那么有没有比较快的方法,有,下面给大家介绍两个,第一,首先是很巧妙的方法,是翻转法。

1、翻转法:

我要移动3位  那我们就对前n-k个逆置 得到 4 3 2 1 5 6 7 再对k个之后的数字进行逆置,4 3 2 1 7 6 5  再整体逆置 得到 5 6 7 1 2 3 4 是不是很巧妙,我们来实现以下,看以下代码:

void reverse(int* nums,int left , int right)
{while(left<right){int tmp = nums[left];nums[left]=nums[right];nums[right] = tmp;left++;right--;}
}
void rotate(int* nums, int numsSize, int k) 
{if(k>numsSize)k%=numsSize;reverse(nums,0,numsSize-k-1);reverse(nums,numsSize-k,numsSize-1);reverse(nums,0,numsSize-1);
}

还有一种方法,就是我们把前k个拷贝到新数组中去,再把后k个也拷贝到新数组中去,最后一起拷贝到数组中即可。

void rotate(int* nums, int numsSize, int k) 
{if(k>numsSize)k%=numsSize;int* tmp = (int*)malloc(sizeof(int)*numsSize);memcpy(tmp,nums+numsSize-k,k*sizeof(int));memcpy(tmp+k,nums,(numsSize-k)*sizeof(int));memcpy(nums,tmp,numsSize*sizeof(int));free(tmp);tmp=NULL;
}

总结

1、时间复杂度和空间复杂度的概念

2、大O渐进法

3、大家一定要去做一下OJ题目,去看一看算法题应该怎么写。

如果这份博客对大家有帮助,希望各位给小马一个大大的点赞鼓励一下,如果喜欢,请收藏一下,谢谢大家!!!
制作不易,如果大家有什么疑问或给小马的意见,欢迎评论区留言。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/6346.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

无人机+无人车:自组网协同技术及应用前景详解

无人车&#xff0c;也被称为自动驾驶汽车、电脑驾驶汽车或轮式移动机器人&#xff0c;是一种通过电脑系统实现无人驾驶的智能汽车。这种汽车依靠人工智能、视觉计算、雷达、监控装置和全球定位系统协同合作&#xff0c;使得电脑可以在没有任何人类主动操作的情况下&#xff0c;…

C++基础——输入输出(文件)

一、标准输入输出流 C 的输入输出是程序与用户或外部设备&#xff08;如文件、网络等&#xff09;之间交换信息的过程。 C 提供了丰富的标准库来支持这种交互&#xff0c;主要通过流的概念来实现。 流&#xff1a;抽象概念&#xff0c;表示一连串的数据&#xff08;字节或字…

typescript:vscode的settings配置文件配置ts语法提示

typescript&#xff1a;vscode的settings配置文件配置ts语法提示 1 找到vscode左下角的齿轮按钮 2 点击Settings&#xff08;或者快捷键ctrl,&#xff09;&#xff1a; 点击右上角的Open Settings(JSON)按钮打开配置文件&#xff1a; 或者ctrlshiftp&#xff0c;搜索settings&…

展开说说:Android实现多线程几种方式

1、线程是什么 关于线程&#xff0c;Android开发经常遇到的一个和线程相关的异常报错&#xff1a;NetworkOnMainThreadException&#xff0c;因为网络请求不可以运行在主线程&#xff08;又称UI线程&#xff09;。和网络请求一样的还有I/O操作、数据库操作等耗时任务一样都只能…

奈氏准则和香农定理

一、奈奎斯特和香农 哈里奈奎斯特&#xff08;Harry Nyquist&#xff09;(左) 克劳德艾尔伍德香农&#xff08;Claude Elwood Shannon&#xff09;(右) 我们应该在心里记住他们&#xff0c;记住所有为人类伟大事业做出贡献的人&#xff0c;因为他们我们的生活变得越来越精彩&…

使用xshell工具连接ubuntu的root账户被拒绝的解决方法

问题描述&#xff1a; 我在使用xshell工具远程连接Ubuntu虚拟机的过程中&#xff0c;如果连接的是的普通用户则xshell工具可以正常连接&#xff0c;但是当我向连接ubuntu系统的root用户&#xff0c;即便是密码输入正确但还是不能连接成功。不能连接成功的截图如下&#xff1a; …

从零开始学AI绘画,万字Stable Diffusion终极教程(二)

【第2期】关键词 欢迎来到SD的终极教程&#xff0c;这是我们的第二节课 这套课程分为六节课&#xff0c;会系统性的介绍sd的全部功能&#xff0c;让你打下坚实牢靠的基础 1.SD入门 2.关键词 3.Lora模型 4.图生图 5.controlnet 6.知识补充 在第一节课里面&#xff0c;我们…

PS 2018

软件安装 文件太大&#xff0c;分批上传了&#xff0c;后续下载下来文件目录是这样的&#xff0c; 三个文件夹.7z 分批上传&#xff0c;exe也压缩分批上传&#xff0c; 其中products文件夹太大&#xff0c;里面子目录继续压缩分批上传 都下好了&#xff0c;就exe执行安装就行…

如何使用提示测试为LLMs构建单元测试?

原文地址&#xff1a;how-to-build-unit-tests-for-llms-using-prompt-testing 确保您的人工智能交付&#xff1a;快速测试完美生成应用程序的基本指南 2024 年 4 月 26 日 如果你曾经编写过软件&#xff0c;你就会知道测试是开发过程中必不可少的一部分。特别是单元测试&#…

Git推送本地项目到gitee远程仓库

Git 是一个功能强大的分布式版本控制系统&#xff0c;它允许多人协作开发项目&#xff0c;同时有效管理代码的历史版本。开发者可以克隆一个公共仓库到本地&#xff0c;进行更改后将更新推送回服务器&#xff0c;或从服务器拉取他人更改&#xff0c;实现代码的同步和版本控制。…

BUUCTF:Web 解析(一)

前言 Buuctf Web 是一个在线安全挑战平台&#xff0c;旨在提高参与者对网络安全的理解和实践能力。本文将详细介绍 Buuctf Web 的特点、挑战和机遇&#xff0c;帮助读者更好地了解这一领域。 一、Buuctf Web 的特点 多样化的挑战场景&#xff1a;Buuctf Web 提供了多种挑战场…

金属表面粗糙度对信号的影响

在进行PCB的传输线设计时&#xff0c;如果希望仿真结果更加贴合于实际的效果&#xff0c;就需要考虑很多的附加因素&#xff0c;比如&#xff0c;真实的叠构参数、介电常数、损耗角正切值、蚀刻因子、金属表面粗糙度、玻纤效应等&#xff0c;在常规的信号仿真中&#xff0c;前三…

springboot整合mybatis配置多数据源(mysql/oracle)

目录 前言导入依赖坐标创建mysql/oracle数据源配置类MySQLDataSourceConfigOracleDataSourceConfig application.yml配置文件配置mysql/oracle数据源编写Mapper接口编写Book实体类编写测试类 前言 springboot整合mybatis配置多数据源&#xff0c;可以都是mysql数据源&#xff…

基于RK1126的小型化低功耗AI相机,支持人体特征识别、人脸特征识别、案例帽识别等

提供可定制的⼀套 AI相机软硬件开发平台&#xff0c; 硬件采⽤ RockchipRV1126处理器解决 ⽅案&#xff0c;可选择搭配 SonyIMX系列传感器&#xff0c;POE供电与数据传输&#xff0c;采⽤ 38板标准结构设计&#xff0c;快速按需定制外壳&#xff0c;⽀撑从开发到验证到批量⽣产…

C#知识|事件集中响应,多个按钮关联同一事件(实例练习)

哈喽&#xff0c;你好&#xff0c;我是雷工&#xff01; 本节学习窗体Controls集合、控件事件的统一关联及如何优化重复代码。 01 事件集中响应 原理&#xff1a;就是相同的控件&#xff0c;可以关联同一个事件响应方法。 02 示例演示 2.1、示例功能 该示例实现窗体中选择…

基于php+mysql+html简单图书管理系统

博主介绍&#xff1a; 大家好&#xff0c;本人精通Java、Python、Php、C#、C、C编程语言&#xff0c;同时也熟练掌握微信小程序、Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我有丰富的成品Java、Python、C#毕设项目经验&#xff0c;能够为学生提供各类…

Springboot图片上传【本地+oss】

文章目录 1 前端组件页面2 本地上传3 上传到阿里云oss3.1申请开通账号&#xff0c;做好先导准备3.2 开始使用 1 前端组件页面 使用的VueElement组件 在线cdn引入&#xff1a; <script src"https://cdn.bootcdn.net/ajax/libs/vue/2.7.16/vue.js"></script&…

Typescript语法

常量声明 let用于声明变量&#xff0c;而const用于声明常量。两者的区别是变量在赋值后可以修改&#xff0c;而常量在赋值后便不能修改。 const b:number 200; 类型判断 如果一个变量或常量的声明包含了初始值&#xff0c;TS便可以根据初始值进行类型判断&#xff0c;此时…

有限元分析

TOC在这里插入代码片 **应力挤压时的横截面内部力 剪应力&#xff1a;应力的相反力 法向力&#xff1a;施加的压力 与应力垂直 你好&#xff01; 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章&#xff0c;了解…