时间复杂度和空间复杂度详解
有一堆数据需要排序,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)})\} {…...

CTFer成长之路之Python中的安全问题
Python中的安全问题CTF 1.Python里的SSRF 题目提示 尝试访问到容器内部的 8000 端口和 url path /api/internal/secret 即可获取 flag 访问url: http://f5704bb3-5869-4ecb-9bdc-58b022589224.node3.buuoj.cn/ 回显如下: 通过提示构造payload&…...

SpringBoot知识快速复习
Spring知识快速复习启动器自动装配ConfigurationImport导入组件Conditional条件装配ImportResource导入Spring配置文件ConfigurationProperties配置绑定Lombok简化开发dev-toolsyaml请求和响应处理静态资源规则与定制化请求处理-Rest映射请求处理-常用参数注解使用请求处理-Ser…...

SpringBoot+React博客论坛系统 附带详细运行指导视频
文章目录一、项目演示二、项目介绍三、项目运行截图四、主要代码一、项目演示 项目演示地址: 视频地址 二、项目介绍 项目描述:这是一个基于SpringBootReact框架开发的博客论坛系统。首先,这是一个前后端分离的项目,文章编辑器…...

C++ primer 之 extern
C primer 之 extern什么是声明什么是定义两者有什么区别ertern的作用什么是声明 就是使得名字为程序所知,一个文件如果想使用别处定义的名字就必须包含对那个名字的声明。 什么是定义 负责创建与名字关联的实体。 两者有什么区别 变量声明和声明都规定了变量的…...

Linux 练习二 (VIM编辑器 + GCC编译器 + GDB调试)
文章目录VIM命令思维导图GCC编译器1、GCC编译文件练习2、静态库动态库制作练习将此函数编译成动态库将此函数编译成静态库GCC优化选项 -OnGDB调试命令练习练习一:编写一个程序,通过gdb调试,使用到gdb的b,n,s࿰…...

python3 连接数据库 mysql PyMysql
python3PyMysql PyMySQL 是在 Python3.x 版本中用于连接 MySQL 服务器的一个库 , 遵循 Python 数据库 API v2.0 规范 。 PyMySQL 安装 pip install PyMySQLPyMySQL 连接数据库 import pymysql pymysql.Connect(hostlocalhost,port 3306,user root,password **…...

昇腾AI新技能,还能预防猪生病?
国药集团动物保健股份有限公司(简称“国药动保”)是专业从事动物保健产品研发、生产和销售的国家高新技术企业,是国内少数几家具备新产品原创能力的动物保健企业。其中,猪圆环病毒灭活疫苗等市场份额位居行业前列。 “猪圆环病毒…...

模板方法模式(Template Method)
模式结构图 说明 基本方法是模板方法的组成部分。基本方法分为一下三种: 抽象方法 由抽象类声明,由其具体子类实现。C中就是纯虚函数。 具体方法 由抽象类或具体类声明并实现,子类可以进行覆盖也可以继承。C中是虚函数。 钩子方法 由抽象类…...

C C++ typedef的使用
一、为基本数据类型起别名 typedef int myint; myint x 5; "myint"是"int"的别名,可以使用"myint"来代替"int"声明变量,这个很好理解,但是也很少有人这么用吧。 二、为结构体起别名 …...

Laravel框架03:DB类操作数据库
Laravel框架03:DB类操作数据库一、概述二、数据表的创建与配置三、增删改操作1. 增加信息2. 修改数据3. 删除数据四、查询操作1. 取出基本数据2. 取出单行数据3. 获取一个字段的值4. 获取多个字段的值5. 排序6. 分页五、执行任意的SQL语句一、概述 按照MVC的架构&a…...