时间复杂度和空间复杂度详解
有一堆数据需要排序,A要使用快速排序,B要使用堆排序,A认为自己的代码更高效,B也认为自己的代码更高效,在这种情况下,怎么来判断谁的代码更好一点呢?这时候就有了时间复杂度和空间复杂度。
目录
一、算法效率
1.1 衡量算法好坏的关键
1.2 算法的复杂度
二、时间复杂度
2.1 时间复杂度的概念
2.2 大O的渐进表示法
2.3 常见的时间复杂度的计算案例
2.3.1 案例一
2.3.2 案例二
2.3.3 案例三
2.3.4 案例四
2.3.5 案例五
2.3.6 案例六
2.3.7 案例七
2.3.8 案例八
三、空间复杂度
3.1 空间复杂度的概念
3.2 常见的空间复杂度计算案例
3.2.1 案例一
3.2.2 案例二
3.2.3 案例三
3.2.4 案例四
四、常见的复杂度对比
五、复杂度的练习
5.1 消失的数字
5.2 旋转数组
一、算法效率
1.1 衡量算法好坏的关键
我们在之前探讨过斐波那契数列计算的两种方式即递归和循环,这两种方式那种更好呢?我们认为循环的方式更好,使用递归去求斐波那契数列,会有大量的重复计算,那么显然循环的方式更好,这两个代码可以直接分析出来,但是对于其他一些不容易进行分析的算法,我们用什么来作为算法好坏的标准呢?这里就引入了算法复杂度的概念。
1.2 算法的复杂度
算法在编写成可执行程序后,运行时需要耗费时间资源和空间资源。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。在下面的文章中我们会依次讲解时间复杂度和空间复杂度。
二、时间复杂度
2.1 时间复杂度的概念
时间复杂度不是算的算法所用的时间,在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一 个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知 道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
对于算法的时间复杂度,我们不需要计算出精确的执行次数,只需要对执行次数进行估算,得到大致的量级,在计算时间复杂度和空间复杂度的时候,我们统一使用的是大O的渐进表示法。
2.2 大O的渐进表示法
在计算算法的复杂度的时候,我们只需要得到大概次数所属的量级,即忽落掉一些对结果影响不大的项,在这里我们主要使用大O的渐进表示法。
大O符号(Big O notation):是用于描述函数渐进行为的数学符号。
推导大O阶方法:
1.用常数1取代运行时间中的所有加法常数。
2.在修改后的运行次数函数中,只保留最高阶项。
3.如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。
对于有一些算法,算法的时间复杂度存在最好、平均和最坏情况:
1.最坏情况:任意输入规模的最大运行次数(上界)
2.平均情况:任意输入规模的期望运行次数
3.最好情况:任意输入规模的最小运行次数(下界)
2.3 常见的时间复杂度的计算案例
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);
}
在上面的概念中我们知道时间复杂度就是基本语句的执行次数,在上述算法中,有两个循环,一个for循环,执行2*N次,一个while循环,执行10次,所以执行次数为2*N+10次,计算时间复杂度我们使用大O的渐进表示法,忽略掉对整体影响不大的项,所以在此算法中的时间复杂度为:O(N)。
2.3.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);
}
在上述算法中,有两个for循环,按照大O的渐进表示法,上述算法的时间复杂度是:O(M+N),在此案例中并未对M和N的大小进行说明,如果题目指明M远大于N,那就说明N对最终结果影响不大,可以忽略,时间复杂度就是:O(M);如果题目指明N远大于M,那就说明M对最终结果影响不大,可以忽略,时间复杂度就是:O(N);如果题目指明M=N,时间复杂度就是:O(N)或者O(M)。
2.3.3 案例三
// 计算Func4的时间复杂度?
void Func4(int N)
{int count = 0;for (int k = 0; k < 100; ++k){++count;}printf("%d\n", count);
}
上述算法中,基本语句的执行次数为100,按照大O的渐进表示法,时间复杂度为:O(1)。
在这里如果有同学不理解常数阶的执行次数为什么一律用O(1)来表示,实际上,CPU是足够快的,循环100000000次和循环100次对他而言,时间都差不多,所以他会认为是一个量级,所以常数阶都用O(1)来表示。
2.3.4 案例四
// 计算strchr的时间复杂度?
const char * strchr ( const char * str, int character );
上述的strchr库函数的功能,参数介绍如下图:
strchr函数相当于:
while(str)
{if(*str == character)return str;elsestr++;
}
上述函数的时间复杂度我们需要分情况来看:
//假设有一个字符串
a d c g e h e a x \0
//如果我们要在上述字符串中找a字符,那么只需要找1次就可以找到,时间复杂度为O(1),这个情况属于最好的情况。
//如果我们要在上述字符串中找x字符,那么只需要找N(假设一共有N个字符)次就可以找到,时间复杂度为O(N),这个情况属于最坏的情况。
对于时间复杂度,我们一般关注的是最坏时间复杂度,所以此算法的时间复杂度为O(N)。
2.3.5 案例五
// 计算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个数就需要N-1趟冒泡排序,在这里是冒泡排序的优化版本(如果想要对冒泡排序了解的更加清晰可以看看我的《数组》那篇博文),如果在进行循环的过程中,数组已经是升序那么接下来的循环就不会再继续,对于上述时间复杂度的计算,我们主要通过思想来进行分析,分为最好情况和最坏情况,如下图:
从上图我们就可以看出,在最好情况下,只需要进行一趟冒泡排序,即N-1次的比较,所以最好时间复杂度为O(1),在最坏情况下,我们需要进行N-1趟冒泡排序,每趟冒泡排序需要对两两相邻的数进行比较,最坏情况下的执行次数为:N-1+N-2+N-3+......3+2+1=N*(N-1)/2,按照大O的渐进表示法,此算法的最坏时间复杂度是:O(N^2)。
对于时间复杂度,我们一般关注的是最坏时间复杂度,所以此算法的时间复杂度为O(N^2)。
2.3.6 案例六
// 计算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;elsereturn mid;}return -1;
}
二分查找是什么?二分查找,例如我们需要在arr= {1,2,3,4,5,6,7,8,9,10}中找到7对应的下标,此时我们先看中间的数即arr[4],此时数组中arr[4]对应的数字是5,比7小,我们从arr[5]到arr[9]范围内寻找,再次找到arr[5]到arr[9]中间的数,即arr[7],对应的数字是8,比7大,我们从arr[5]到arr[6]的范围找,再找arr[5]到arr[6]中间的数即arr[5],arr[5]对应的数为6比7小,此时再将arr[6]对应的数字与7比较,相等,二分查找的前提是有序。
上述算法是二分查找的算法,这个算法也分为最好情况和最坏情况,最好情况是进行一次二分查找就能够找到,此时最好时间复杂度是:O(1)。
接下来我们通过分析二分查找的算法来计算它的最坏时间复杂度,
二分查找的时间复杂度为O(logN)。
2.3.7 案例七
// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{if(0 == N)return 1;return Fac(N-1)*N;
}
上述算法涉及递归我们用图来解释:
2.3.8 案例八
// 计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{if(N < 3)return 1;return Fib(N-1) + Fib(N-2);
}
此处补充斐波那契数列的知识:斐波那契数列指的是这样一个数列:0、1、1、2、3、5、8、13、21、34、55、89……满足:从第3项起,每一项都等于前两项之和,我们可以使用递归来计算斐波那契数列。
使用递归求斐波那契数列的调用如下:
需要注意的是,在实际调用过程中,如果我们要求Fib(5),图示如下:
调用的模型类似于下方:
三、空间复杂度
3.1 空间复杂度的概念
空间复杂度也是一个数字表达式,是对一个算法在运行过程中临时占用存储空间大小的量度(是这个算法额外开辟的空间),空间复杂度不是程序占用了多少bytes的空间,他计算的是变量的大小,空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显示申请的额外空间来确定。
3.2 常见的空间复杂度计算案例
3.2.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;}
}
上述算法的空间复杂度是:O(1),空间复杂度算的是由于算法额外需要开辟的空间,即由于算法需要额外开辟的变量个数,在上述算法中我们额外开辟了三个变量:end、exchange、i,按照大O的渐进表示法,空间复杂度即为:O(1)。
在这里许多人会有一个误区:把函数的参数也算作额外开辟变量的个数,这种想法显然不对,函数的参数属于数据源,即给一些数据,按照算法对这些数据进行一定的处理,最后得到想要结果,这些数据不属于算法所需要的额外开辟的空间。
3.2.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;
}
上述算法中,额外开辟了n+3个变量,首先开辟了n+1个long long类型的数据,其次开辟了一个long long*类型的变量fibArray和int类型的变量i,按照大O的渐进表示法,空间复杂度为:O(n)。
3.2.3 案例三
// 计算斐波那契递归Fib的空间复杂度?
long long Fib(size_t N)
{if(N < 3)return 1;return Fib(N-1) + Fib(N-2);
}
求斐波那契递归的空间复杂度,我们依然先看递归的调用图示:
对于上述算法,它的空间复杂度是:O(N),主要是由于它的调用顺序:
对于Fib(N),调用时先调用 Fib(N-1),然后调用Fib(N-2),一直到调用到Fib(2),然后返回Fib(2)的值,把Fib(2)函数调用开辟的函数帧还给存储空间,然后调用Fib(1),也就是说Fib(2)和Fib(1)是使用的同一块空间,以此类推,在此函数递归调用的过程中,有一些调用复用同一块空间,所以实际上该算法额外开辟的空间为N个,即空间复杂度为:O(N)。
此处主要还涉及了一些关于函数栈帧的创建与销毁的知识,这部分知识大家可以从“我的资源”中找到,或者私聊我要相关的笔记。
此处我们还可以通过求解Fib(5)来演示:
3.2.4 案例四
// 计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{if (N == 0)return 1;return Fac(N - 1) * N;
}
上述的函数递归调用与案例三有所不同,在本案例中,函数调用层层嵌套,此函数递归调用的方式类似下图:
对于上述函数的每次调用我们可以认为开辟了常数个变量,需要进行N次函数调用,所以上述算法的空间复杂度是:O(N)。
四、常见的复杂度对比

(上图来源于百度,如有侵权,告知删除)
五、复杂度的练习
5.1 消失的数字
oj链接:https://leetcode-cn.com/problems/missing-number-lcci/
5.2 旋转数组
oj链接:https://leetcode-cn.com/problems/rotate-array/
注:上面两个题大家可以先做一下,他们将在下一篇博客中具体分析。
相关文章:

时间复杂度和空间复杂度详解
有一堆数据需要排序,A要使用快速排序,B要使用堆排序,A认为自己的代码更高效,B也认为自己的代码更高效,在这种情况下,怎么来判断谁的代码更好一点呢?这时候就有了时间复杂度和空间复杂度。 目录 …...
【C++】面向对象---封装
【C】面向对象—封装 1.封装的意义 封装是C面向对象三大特性之一 封装的意义: 将属性和行为作为一个整体,表现生活的事物将属性和行为加以权限控制 封装意义一: 在设计类的时候,属性和行为写在一起,表现事物 语…...
Docker简介
一、介绍容器虚拟化技术(带环境安装的一种解决方案)打破程序即应用的观念,透过镜像image将作业系统核心除外,运用应用程序所需要的运行环境,由上而下打包,达到应用程序跨平台间的无缝接轨运作。Docker是基于…...

量化学习(一)数据获取
试验环境 windows10 AnacondaPyCharm(小白参考文章:https://coderx.com.cn/?p14) VM中安装MySQL5.7(设置utf8及相应配置优化) 关于复权 小白参考文章:https://zhuanlan.zhihu.com/p/469820288 数据来源 AK…...
java并发编程讨论:锁的选择
java并发编程 线程堆栈大小 单线程的堆栈大小默认为1M,1000个线程内存就占了1G。所以,受制于内存上限,单纯依靠多线程难以支持大量任务并发。 上下文切换开销 ReentrantLock 2个线程交替自增一个共享变量,使用ReentrantLock&…...

大数据框架之Hadoop:MapReduce(三)MapReduce框架原理——ReduceTask工作机制
1、ReduceTask工作机制 ReduceTask工作机制,如下图所示。 (1)Copy阶段:ReduceTask从各个MapTask上远程拷贝一片数据,并针对某一片数据,如果其大小超过一定阈值,则写到磁盘上,否则直…...

Nginx的介绍、安装与常用命令
前言:传统结构上(如下图所示)我们只会部署一台服务器用来跑服务,在并发量小,用户访问少的情况下基本够用但随着用户访问的越来越多,并发量慢慢增多了,这时候一台服务器已经不能满足我们了,需要我们增加服务…...

less基础
一、less介绍 1、介绍 是css预处理语言,让css更强大,可以实现在less里面定义变量函数运算等 2、less默认浏览器不识别 less转成csS (框架: less/sass 框架的内置了转码less-css) 3、使用语法 1.创建less文件xxx.less 后缀.less 2. less编译成css 再引入…...

电子统计台账:海量数据中导入特定行,极力减少键盘编辑工作量
1 前言从事企业统计工作的小伙伴,本来已经够忙的了,现在又要加上什么电子台账这种鬼任务,而且居然还要每月来一次,简直不能忍。如果非要捏着鼻子忍了,那么有什么办法,减轻工作量?2 问题的提出有…...

ChatGPT是如何训练得到的?通俗讲解
首先声明喔,我是没有任何人工智能基础的小白,不会涉及算法和底层原理。 我依照我自己的简易理解,总结出了ChatGPT是怎么训练得到的,非计算机专业的同学也应该能看懂。看完后训练自己的min-ChatGPT应该没问题 希望大牛如果看到这…...

刷题28-有效的变位词
32-有效的变位词 解题思路: 注意变位词的条件,当两个字符串完全相等或者长度不等时,就不是变位词。 把字符串中的字符映射成整型数组,统计每个字符出现的次数 注意数组怎么初始化: int [] s1new int[26]代码如下&a…...

JavaWeb中异步交互的关键——Ajax
文章目录1,Ajax 概述1.1 作用1.2 同步和异步1.3 案例1.3.1 分析1.3.2 后端实现1.3.3 前端实现2,axios2.1 基本使用2.2 快速入门2.2.1 后端实现2.2.2 前端实现2.3 请求方法别名3,JSON3.1 概述3.2 JSON 基础语法3.2.1 定义格式3.2.2 代码演示3.2.3 发送异步…...

python爬虫常见错误
python爬虫常见错误前言python常见错误1. AttributeError: WebDriver object has no attribute find_element_by_id1. 问题描述2. 解决办法2. selenium:DeprecationWarning: executable_path has been deprecated, please pass in1. 问题描述2. 解决办法3. 下载了包…...

AI_Papers周刊:第三期
CV - 计算机视觉 | ML - 机器学习 | RL - 强化学习 | NLP 自然语言处理 2023.02.20—2023.02.26 文摘词云 Top Papers Subjects: cs.CL 1.LLaMA: Open and Efficient Foundation Language Models 标题:LLaMA:开放高效的基础语言模型 作者&#…...
在win7上用VS2008编译skysip工程
在win7上用VS2008编译skysip工程 1. 安装vs2008及相应的补丁包,主要包含以下安装包: 1.1 VS2008TeamSuite90DayTrialCHSX1429243.iso 1.2 VS2008SP1CHSX1512981.iso 1.3 VS90sp1-KB945140-CHS.exe 2. 安装Windows SDK: 6.0.6001.18000.367-KRMSDK_EN.zip 例如安装路径为…...
python 数据结构习题
旋转图像给定一个nn的二维矩阵表示一个图像。将图像顺时针旋转90度。你必须在原地旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要使用另一个矩阵来旋转图像。例如,给定matrix[[1,2,3],[4,5&#x…...

18、MySQL8其它新特性
文章目录1 MySQL8新特性概述1.1 MySQL8.0 新增特性1.2 MySQL8.0移除的旧特性2 新特性1:窗口函数2.1 使用窗口函数前后对比2.2 窗口函数分类2.3 语法结构2.4 分类讲解1 序号函数2 分布函数3 前后函数4 首尾函数5 其他函数2.5 小 结3 新特性2:公用表表达式…...
【Android笔记79】Android之接口请求库Retrofit的介绍及使用
这篇文章,主要介绍Android之接口请求库Retrofit的介绍及使用。 目录 一、Retrofit接口请求库 1.1、什么是Retrofit 1.2、Retrofit的使用 (1)引入依赖...

蓝桥杯 考勤打卡
问题描述 小蓝负责一个公司的考勤系统, 他每天都需要根据员工刷卡的情况来确定 每个员工是否到岗。 当员工刷卡时, 会在后台留下一条记录, 包括刷卡的时间和员工编号, 只 要在一天中员工刷过一次卡, 就认为他到岗了。 现在小蓝导出了一天中所有员工的刷卡记录, 请将所有到岗…...

逻辑回归
逻辑回归 在分类问题中,要预测的变量y为离散值(y0~1),逻辑回归模型的输出变量范围始终在 0 和 1 之间。 训练集为 {(x(1),y(1)),(x(2),y(2)),...,(x(m),y(m))}\{(x^{(1)},y^{(1)}),(x^{(2)},y^{(2)}),...,(x^{(m)},y^{(m)})\} {…...
【杂谈】-递归进化:人工智能的自我改进与监管挑战
递归进化:人工智能的自我改进与监管挑战 文章目录 递归进化:人工智能的自我改进与监管挑战1、自我改进型人工智能的崛起2、人工智能如何挑战人类监管?3、确保人工智能受控的策略4、人类在人工智能发展中的角色5、平衡自主性与控制力6、总结与…...
【论文笔记】若干矿井粉尘检测算法概述
总的来说,传统机器学习、传统机器学习与深度学习的结合、LSTM等算法所需要的数据集来源于矿井传感器测量的粉尘浓度,通过建立回归模型来预测未来矿井的粉尘浓度。传统机器学习算法性能易受数据中极端值的影响。YOLO等计算机视觉算法所需要的数据集来源于…...

Unity | AmplifyShaderEditor插件基础(第七集:平面波动shader)
目录 一、👋🏻前言 二、😈sinx波动的基本原理 三、😈波动起来 1.sinx节点介绍 2.vertexPosition 3.集成Vector3 a.节点Append b.连起来 4.波动起来 a.波动的原理 b.时间节点 c.sinx的处理 四、🌊波动优化…...
深入理解Optional:处理空指针异常
1. 使用Optional处理可能为空的集合 在Java开发中,集合判空是一个常见但容易出错的场景。传统方式虽然可行,但存在一些潜在问题: // 传统判空方式 if (!CollectionUtils.isEmpty(userInfoList)) {for (UserInfo userInfo : userInfoList) {…...

如何应对敏捷转型中的团队阻力
应对敏捷转型中的团队阻力需要明确沟通敏捷转型目的、提升团队参与感、提供充分的培训与支持、逐步推进敏捷实践、建立清晰的奖励和反馈机制。其中,明确沟通敏捷转型目的尤为关键,团队成员只有清晰理解转型背后的原因和利益,才能降低对变化的…...

【Veristand】Veristand环境安装教程-Linux RT / Windows
首先声明,此教程是针对Simulink编译模型并导入Veristand中编写的,同时需要注意的是老用户编译可能用的是Veristand Model Framework,那个是历史版本,且NI不会再维护,新版本编译支持为VeriStand Model Generation Suppo…...

《Docker》架构
文章目录 架构模式单机架构应用数据分离架构应用服务器集群架构读写分离/主从分离架构冷热分离架构垂直分库架构微服务架构容器编排架构什么是容器,docker,镜像,k8s 架构模式 单机架构 单机架构其实就是应用服务器和单机服务器都部署在同一…...

若依登录用户名和密码加密
/*** 获取公钥:前端用来密码加密* return*/GetMapping("/getPublicKey")public RSAUtil.RSAKeyPair getPublicKey() {return RSAUtil.rsaKeyPair();}新建RSAUti.Java package com.ruoyi.common.utils;import org.apache.commons.codec.binary.Base64; im…...
32单片机——基本定时器
STM32F103有众多的定时器,其中包括2个基本定时器(TIM6和TIM7)、4个通用定时器(TIM2~TIM5)、2个高级控制定时器(TIM1和TIM8),这些定时器彼此完全独立,不共享任何资源 1、定…...
命令行关闭Windows防火墙
命令行关闭Windows防火墙 引言一、防火墙:被低估的"智能安检员"二、优先尝试!90%问题无需关闭防火墙方案1:程序白名单(解决软件误拦截)方案2:开放特定端口(解决网游/开发端口不通)三、命令行极速关闭方案方法一:PowerShell(推荐Win10/11)方法二:CMD命令…...