【算法】复杂度分析
第一章、如何分析代码的执行效率和资源消耗
我们知道,数据结构和算法解决的是“快”和“省”的问题,也就是如何让代码运行得更快,一级如何让代码更节省计算机的存储空间。因此,执行效率是评价算法好坏的一个非常重要的指标。那么,如何衡量算法的执行效率尼?这里就要用到我们本节要讲的内容:时间复杂度分析和空间复杂度分析。
一、复杂度分析的意义
我们把代码运行一遍,通过监控和统计手段,就能得到算法执行的时间和占用的内存大小,为什么还要做时间复杂度分析,空间复杂度分析呢?这种“纸上谈兵”似的分析方法比实实在在地运行一遍代码得到的数据更准确吗?
实际上,这是两种不同的评估算法执行效率的方式。对于运行代码来统计复杂度的方法,很多有关数据结构和算法的图书还给它起了一个名字:事后统计法。这种统计方法看似可以给出非常精确的数值,但是却有非常大的局限性。
1、测试结果受测试环境的影响很大
在测试环境中,硬件的不同得到的测试结果会有很大的差异。例如,我们用同样一段代码分别在安装了Intel Core i9处理器(CPU)和Intel Core i3处理器的计算机上运行,显然,代码在安装了Intel Core i9处理器的计算机上要比在安装了Intel Core i3处理器的计算机上的执行速度快得多。又如,在某台机器上,a代码执行的速度比b代码快,当我们换到另外一台配置不同的机器上时,可能会得到截然相反的运行结果。
2、测试结果受测试数据的影响很大
我们会在后续章节详细讲解排序算法,这里用它进行举例说明。对同一种排序算法,待排序数据的有序度不一样,排序执行的时间会有很大的差别。在极端情况下,如果数据已经是有序的,那么有些排序算法不需要做任何操作,执行排序的时间就会非常短。除此之外,如果测试数据规模太小,那么测试结果可能无法真实地反应算法的性能。例如,对于小规模的数据排序,插入排序反而比快速排序快!
因此,我们需要一种不依赖具体的测试环境和测试数据就可以粗略地估计算法执行效率的方法。这就是本节要介绍的时间复杂度分析和空间复杂度分析。
二、大O复杂度表示法
如何在不运行代码的情况下,用“肉眼”分析代码后得到一段的执行时间尼?下面用一段非常简单的代码来举例,看一下如何估算代码的执行时间。求1~n的累加和的代码如下所示:
public static int cumulativeSum(int n){int result = 0;for (int i = 1; i <= n; i++){result += i;}return result;
}
从在CPU上运行的角度来看,这段代码的每一条语句执行类似的操作:读数据--运算--写数据。尽管每一条语句对应的执行时间不一样,但是,这里只是粗略估计,我们可以假设每条语句执行的时间一样,为unit_time。在这个假设的基础上,这段代码的总执行时间是多少尼?
执行第2,6行代码分别需要1个unit_time的执行时间;第3,4行代码循环运行了n遍,需要 2n x unit_time的执行时间。因此,这段代码的总执行时间为(2n + 2) x unit_time的执行时间。通过上面的举例分析,我们得到一个规律:一段代码的总的执行时间为T(n)(例子中的(2n + 2) x unit_time)与每一条语句的执行次数(累加数)(例子中的2n + 2)成正比。
按照这个分析思路,我们再来看另一段代码,如下所示:
public static int cal(int n){int sum = 0;int i = 1;int j;for (; i <= n; i++){j = 1;for (; j <= n; j++){sum = sum + (i * j);}}return sum;
}
依旧假设每条语句的执行时间为unit_time,那么这段代码的总的执行时间是多少尼?
对于第2,3,4,11行代码,每行代码需要1个unit_time的执行时间。第5,6行代码循环执行了n遍,需要2n x unit_time的执行时间。第7,8行代码循环执行了n²遍,需要2n² x unit_time的执行时间。因此,整段代码总的执行时间为T(n) = (2n² + 2n + 4) x unit_time。尽管我们不知道unit_timede 具体值,而且,每一条语句执行时间unit_time可能都不尽相同,但是,通过这两段代码执行时间的推导过程,可以得到一个非常重要的规律:
一段代码的执行时间T(n)与每一条语句总的执行次数(累加数)成正比。
我们可以把这个规律总结成一个公式,如下所示:
T(n) = O(f(n))
下面具体解释一下公式。其中,T(n)表示代码执行的总时间;n表示数据规模;f(n)表示每条语句执行次数的累加和,这个值与n有关,因此用f(n)这样一个表达式来表示;公式中的O这个符号,表示代码的执行时间T(n) 与 f(n)成正比。
套用这个大O表示法,第一个例子中的T(n) = (2n + 2) x unit_time = O(2n + 2),第二个例子中的T(n) = (2n² + 2n + 4) x unit_time = O(2n² + 2n + 4)。实际上,大O时间复杂度并不具体表示代码真正的执行时间,而是表示代码执行时间随着数据规模增大的变化趋势,因此,也称为渐进时间复杂度(asymptotic time complexity),简称时间复杂度。
当n很大时,读者可以把它想象成10000,100000,公式中的低阶,常量,系数3部分并不控制增长趋势,因此可以忽略。我们只需要记录一个最大量级。如果用大O表示法表示上面的两段代码的时间复杂度,就可以记为:T(n) = O(n) 和 T(n) = O(n²)。
补充知识:
在数学中,我们经常会听到关于“高阶”、“低阶”、“常量”和“系数”的术语。让我来解释一下:
高阶(High-order):在多项式或函数中,高阶项是指指数较高的项。例如,在多项式 axn+bxn−1+cxn−2+…axn+bxn−1+cxn−2+… 中,axnaxn 就是高阶项,nn 是高阶项的指数。通常来说,当 nn 越大,该项的影响就越显著,因此被称为“高阶”。
低阶(Low-order):与高阶相对应,低阶项是指指数较低的项。在上面的多项式中,bxn−1bxn−1 和 cxn−2cxn−2 就是低阶项。这些项的影响相对较小,因为它们的指数较低。
常量(Constant):常量是没有包含任何变量的项,它们是数学表达式中的固定值。在多项式 axn+bxn−1+cxn−2+…+daxn+bxn−1+cxn−2+…+d 中,dd 就是常量项。
系数(Coefficient):系数是与变量相乘的数字或参数。在多项式 axn+bxn−1+cxn−2+…axn+bxn−1+cxn−2+… 中,aa、bb 和 cc 都是各自项的系数。系数决定了每个变量项的影响程度,它们可以是实数、复数或其他数学结构的成员。
在一个多项式中,通常高阶项对函数的整体形状和行为有着更显著的影响,而低阶项和常量则在更小的尺度上调整函数的细节。系数则决定了每个项的具体贡献。系数决定了变量的比例关系和对整个公式的影响程度。它们可以改变公式的斜率、曲线形状和整体大小。
三、时间复杂度分析方法
前面介绍了时间复杂度的由来和表示方法。现在,我们介绍一下如何分析一段代码的时间复杂度。下面讲解两个比较实用的法则:加法法则和乘法法则。
1、加法法则:代码总的复杂度等于量级最大的那段代码的复杂度
大O复杂度表示方法只表示一种变化趋势。我们通常会忽略公式中的常量,低阶和系数,只记录最大量级。因此,在分析一段代码的时间复杂度的时候,我们也只需要关注循环执行次数最多的那段代码。
我们来看下面这样一段代码。读者可以先试着分析一下这段代码的时间复杂度,然后与作者分析的思路进行比较,看看思路是否一致。
public static int cal1(int n){int sum_1 = 0;int p = 1;for (; p <= 100; ++p){sum_1 = sum_1 + p;}int sum_2 = 0;int q = 1;for (; q <= n; ++q){sum_2 = sum_2 + q;}int sum_3 = 0;int i = 1;int j = 1;for (; i <= n; ++i){j = 1;for (; j <= n; ++j){sum_3 = sum_3 + i * j;}}return sum_1 + sum_2 + sum_3;
}
复杂度分析:
- 2 2 * 100
- 2 2 * n
- 3 2 * n 2 * n^2
- 1
2 * n^2 + 4 * n + 208
上述这段代码分为4部分,分别是求sum_1,sum_2,sum_3,以及对这3个数求和。我们分别分析每一部分代码的时间复杂度,然后把它们放到一起,再取一个量级最大的作为整段代码的时间复杂度。
求sum_1这部分代码的时间复杂度是多少尼?因为这部分代码循环执行了100次(p=100,一直不变,p是个常量),所以执行时间是常量。
这里要再强调一下,即便这段代码循环执行10000次或100000次,只要是一个已知的数,与数据规模n无关,这也是常量级的执行时间。回到大O时间复杂度的概念,时间复杂度表示的是代码执行时间随数据规模(n)的增长趋势,因此,无论常量级的执行时间多长,它本身对增长趋势没有任何影响,在大O复杂度表示法中,我们可以将它(常量)忽略。
求sum_2,sum_3,以及对这3个数求和这三部分代码的时间复杂度分别是多少尼?答案是O(n),O(n²),常量。读者应该很容易就分析出来,就不在赘述了。
综合这4部分代码的时间复杂度,我们取其中最大的量级,因此,整段代码的时间复杂度就为O(n²)。也就是说,总的时间复杂度等于量级最大的那部分代码的时间复杂度。这条法则就是加法法则,用公式表示出来,如下所示:
如果:
T1(n) = O(f(n)); T2(n) = O(g(n))
那么:
T(n) = T1(n) + T2(n) = max(O(f(n)), O(g(n))) = O(max(f(n), g(n)))
2、乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积
我们刚讲了复杂度分析中的加法法则,再来看一下乘法法则,如下所示:
如果:
T1(n) = O(f(n)); T2(n) = O(g(n))
那么:
T(n) = T1(n) X T2(n) = O(f(n))X O(g(n)) = O(f(n) X g(n))
也就是说:假设T1(n) = O(n),T2(n) = O(n²),则T1(n) X T2(n) = O(n³)。落实到具体的代码上,我们可以把乘法法则看成嵌套循环。我们通过例子来解释一下,如下所示
public static void cal(int n){int ret = 0;int i = 1;for (; i <= n; i++){ret = ret + f(i);}
}private static int f(int n) {int sum = 0;int i = 1;for (; i <= n; i++){sum += i;}return sum;
}

我们单独观察上述代码中的cal()函数,在cal()函数的时间复杂度为T1 = O(n),f()函数的时间复杂度为T2(n) = O(n),则总的时间复杂度为T(n) = T1(n) X T2(n) = O(n X n) = O(n²)。
四、几种常见的时间复杂度量级
虽然代码千差万别,但常见的时间复杂度量级并不多。简单总结一下,如图所示,这个涵盖了读者今后可以接触的绝大部分的时间复杂度量级。
计算数量级通常是对一个数的大小进行粗略估计,以确定它属于哪个数量级。这种估计可以通过以下步骤进行:
将数写成科学计数法:将数写成形如 a×10ba×10b 的形式,其中 1≤a<101≤a<10 是尾数,bb 是指数。例如,1234 可以写成 1.234×1031.234×103。
确定尾数 aa 的范围:确定尾数 aa 的范围。通常来说,aa 范围在 1 到 10 之间。
确定指数 bb 的值:指数 bb 表示了数值在数量级上的大小。例如,103103 表示数值在数量级上是千级别的。
确定数量级:根据指数 bb 的值来确定数量级。例如,bb 为 3 表示数值在数量级上是千级别的。
举例来说,假设有一个数值是 6.78×1056.78×105。尾数 aa 是 6.78,指数 bb 是 5。因为 bb 是 5,所以这个数值在数量级上是百万级别的。
注意,计算数量级是一个近似值的过程,因此结果可能不是精确的,但通常足够用于粗略估计
一、时间复杂度
时间复杂度是指执行算法所需要的计算工作量
时间复杂度是用来衡量算法执行时间随着输入大小增加而增加的程度的一个度量。它表示算法的运行时间与输入数据的大小之间的关系。
在计算时间复杂度时,通常考虑最坏情况下的运行时间,因为这能够给出算法的最差执行时间保证。时间复杂度用大O符号表示,通常写作O(f(n)),其中n表示输入大小,f(n)是一个函数,它描述了算法执行所需的时间与n的关系。
例如,一个具有时间复杂度O(n)的算法表示,当输入大小增加n倍时,它的运行时间也将增加n倍。而一个具有时间复杂度O(n^2)的算法表示,当输入大小增加n倍时,它的运行时间将增加n的平方倍。
时间复杂度的计算可以帮助我们选择合适的算法来解决特定问题,并预测算法在实际应用中的性能表现。通常来说,我们会选择具有较低时间复杂度的算法,尤其是当处理大量数据时。
二、空间复杂度
而空间复杂度是指执行这个算法所需要的内存空间。
空间复杂度是衡量算法空间利用率的度量标准,也就是算法在执行过程中所需要的存储空间大小。
在计算空间复杂度时,通常会考虑以下几个因素:
算法本身所需要的空间:例如程序中定义的变量、数组、对象等。
输入数据所占用的空间:例如在排序算法中,需要占用额外的数组空间来存储输入数据。 算法执行过程中所占用的空间:例如在递归算法中,每个递归调用都需要分配额外的栈空间。
空间复杂度通常用大O符号(O)表示,与时间复杂度类似。例如,如果一个算法的空间复杂度为O(n),则它所需要的存储空间与输入数据的大小n成正比。
在实际应用中,除了考虑算法的时间复杂度之外,也需要考虑空间复杂度。对于内存有限的嵌入式系统或移动设备等场景,空间复杂度的控制非常重要,因为过高的空间复杂度会导致程序崩溃或无法运行。
相关文章:
【算法】复杂度分析
第一章、如何分析代码的执行效率和资源消耗 我们知道,数据结构和算法解决的是“快”和“省”的问题,也就是如何让代码运行得更快,一级如何让代码更节省计算机的存储空间。因此,执行效率是评价算法好坏的一个非常重要的指标。那么&…...
车载电子测试学习内容
搜集了一些车载测试的学习内容,大家可以参考。...
STM32F103x 的时钟源
AHB (Advanced High-performance Bus) 高速总线,用来接高速外设的。 APB (Advanced Peripheral Bus) 低速总线,用来接低速外设的,包含APB1 和 APB2。 APB1:上面连接的是低速外设,包括电源接口、备份接口、 CAN 、 US…...
【电路笔记】-RC放电电路
RC放电电路 文章目录 RC放电电路1、概述2、RC放电电路3、RC放电电路示例当电压源从完全充电的 RC 电路中移除时,电容器 C 将通过电阻 R 放电。 1、概述 RC 放电电路利用电阻器-电容器组合的固有 RC 时间常数以指数衰减率对电容器进行放电。 在之前的 RC 充电电路教程中,我们…...
【C++STL】STL容器详解
创作不易,本篇文章如果帮助到了你,还请点赞 关注支持一下♡>𖥦<)!! 主页专栏有更多知识,如有疑问欢迎大家指正讨论,共同进步! 🔥c系列专栏:C/C零基础到精通 🔥 给大…...
缓存篇—缓存雪崩
什么是缓存雪崩 通常我们为了保证缓存中的数据与数据库中的数据一致性,会给 Redis 里的数据设置过期时间,当缓存数据过期后,用户访问的数据如果不在缓存里,业务系统需要重新生成缓存,因此就会访问数据库,并…...
力扣日记2.22-【回溯算法篇】47. 全排列 II
力扣日记:【回溯算法篇】47. 全排列 II 日期:2023.2.22 参考:代码随想录、力扣 47. 全排列 II 题目描述 难度:中等 给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。 示例 1: 输…...
如何理解三大微分中值定理
文章看原文,自己写的只是备份 高等数学强化2:一元函数微分学 中值定理 极值点 拐点_一元函数中值定理-CSDN博客 高等数学强化3:一元函数积分学 P积分-CSDN博客 高等数学强化3:定积分几何应用-CSDN博客...
Linux--自定义shell
shell shell就是操作系统提供给用户与操作系统进行交互的命令行界面。它可以理解为一个用户与操作系统之间的接口,用户可以通过输入命令来执行各种操作,如文件管理、进程控制、软件安装等。Shell还可以通过脚本编程实现自动化任务。 常见的Unix系统中使…...
AIGC 实战:Ollama 和 Hugging Face 是什么关系?
Ollama和 Hugging Face 之间存在着双重关系: 1. Ollama是 Hugging Face 开发并托管的工具: Ollama是一个由 Hugging Face 自行开发的开源项目。它主要用于在本地运行大型语言模型 (LLM),特别是存储在 GPT 生成的统一格式 (GPT-Generated Un…...
Gitee教程2(完整流程)
1.配置git git config --global user.name "用户名" git config --global user.email "密码" 如何获取? gitee右上角加号点击新建仓库,仓库名随便起一个就行 找到这条命令,把这两句一个一个复制到vscode终端就行 2.创建g…...
Go 1.22 中的 for 循环新特性详解
目录 每次迭代都创建新变量 支持整数类型循环 小结 在 Go 语言中,for 循环是实现迭代的主要方式。Go 中的 for 循环非常灵活,有多种使用方式,包括传统的三部分 for 循环、类似于其他语言中的 while 循环以及迭代集合的 range 循环。 在 1…...
igolang学习2,golang开发配置国内镜像
go env -w GO111MODULEon go env -w GOPROXYhttps://goproxy.cn,direct...
R语言空间分析、模拟预测与可视化
随着地理信息系统(GIS)和大尺度研究的发展,空间数据的管理、统计与制图变得越来越重要。R语言在数据分析、挖掘和可视化中发挥着重要的作用,其中在空间分析方面扮演着重要角色,与空间相关的包的数量也达到130多个。在本…...
体育赛事直播系统软件开发
体育赛事直播系统的软件开发是一个复杂的项目,需要多个方面的准备和工作。以下是开发这样一个系统可能涉及的主要步骤和考虑因素: 需求分析和规划:首先需要明确系统的功能需求,包括直播视频的流媒体处理、用户管理、直播赛事安排…...
使用 kind 集群安装运行极狐GitLab Runner【上】
GitLab 是一个全球知名的一体化 DevOps 平台,很多人都通过私有化部署 GitLab 来进行源代码托管。极狐GitLab 是 GitLab 在中国的发行版,专门为中国程序员服务。可以一键式部署极狐GitLab。 关于 kind kind 是一个用来运行本地 Kubernetes 机群的工具&a…...
wine 源码 vk3d wine-gecko wine-mono 各版本 国内下载地址 中国科技技术大学源
下载地址 Index of /wine/...
【ArcGIS微课1000例】0104:二位面状数据转三维多面体(建筑物按高度拉伸)
文章目录 一、加载数据二、添加高度字段三、三维拉伸显示四、生成三维体数据五、注意事项一、加载数据 打开ArcScene,加载配套实验数据(0104.rar中的二维建筑物矢量数据,订阅专栏,获取专栏所有文章阅读权限及配套数据),如下图所示: 二、添加高度字段 本实验将二维数据…...
jquery简介与解析
jQuery是一款流行的JavaScript库,旨在简化客户端脚本编写。它针对DOM操作、事件处理、动画效果和AJAX交互提供了简洁高效的解决方案,使得开发者能够更加便捷地创建交互式网页。 // jQuery小插件:给带有highlight类的元素添加鼠标悬停效果 (fu…...
Apache Commons开源的工具库介绍
Apache Commons 是 Apache 软件基金会主持的一个项目,旨在提供一系列可重用的 Java 组件。这些组件覆盖了从数据封装、文本处理到网络通信等各个方面,是 Java 开发中常用的一系列工具库。Apache Commons 项目下的各个库通常以 "commons-" 开头…...
Cursor实现用excel数据填充word模版的方法
cursor主页:https://www.cursor.com/ 任务目标:把excel格式的数据里的单元格,按照某一个固定模版填充到word中 文章目录 注意事项逐步生成程序1. 确定格式2. 调试程序 注意事项 直接给一个excel文件和最终呈现的word文件的示例,…...
工程地质软件市场:发展现状、趋势与策略建议
一、引言 在工程建设领域,准确把握地质条件是确保项目顺利推进和安全运营的关键。工程地质软件作为处理、分析、模拟和展示工程地质数据的重要工具,正发挥着日益重要的作用。它凭借强大的数据处理能力、三维建模功能、空间分析工具和可视化展示手段&…...
江苏艾立泰跨国资源接力:废料变黄金的绿色供应链革命
在华东塑料包装行业面临限塑令深度调整的背景下,江苏艾立泰以一场跨国资源接力的创新实践,重新定义了绿色供应链的边界。 跨国回收网络:废料变黄金的全球棋局 艾立泰在欧洲、东南亚建立再生塑料回收点,将海外废弃包装箱通过标准…...
相机从app启动流程
一、流程框架图 二、具体流程分析 1、得到cameralist和对应的静态信息 目录如下: 重点代码分析: 启动相机前,先要通过getCameraIdList获取camera的个数以及id,然后可以通过getCameraCharacteristics获取对应id camera的capabilities(静态信息)进行一些openCamera前的…...
华为云Flexus+DeepSeek征文|DeepSeek-V3/R1 商用服务开通全流程与本地部署搭建
华为云FlexusDeepSeek征文|DeepSeek-V3/R1 商用服务开通全流程与本地部署搭建 前言 如今大模型其性能出色,华为云 ModelArts Studio_MaaS大模型即服务平台华为云内置了大模型,能助力我们轻松驾驭 DeepSeek-V3/R1,本文中将分享如何…...
ios苹果系统,js 滑动屏幕、锚定无效
现象:window.addEventListener监听touch无效,划不动屏幕,但是代码逻辑都有执行到。 scrollIntoView也无效。 原因:这是因为 iOS 的触摸事件处理机制和 touch-action: none 的设置有关。ios有太多得交互动作,从而会影响…...
JavaScript 数据类型详解
JavaScript 数据类型详解 JavaScript 数据类型分为 原始类型(Primitive) 和 对象类型(Object) 两大类,共 8 种(ES11): 一、原始类型(7种) 1. undefined 定…...
探索Selenium:自动化测试的神奇钥匙
目录 一、Selenium 是什么1.1 定义与概念1.2 发展历程1.3 功能概述 二、Selenium 工作原理剖析2.1 架构组成2.2 工作流程2.3 通信机制 三、Selenium 的优势3.1 跨浏览器与平台支持3.2 丰富的语言支持3.3 强大的社区支持 四、Selenium 的应用场景4.1 Web 应用自动化测试4.2 数据…...
前端调试HTTP状态码
1xx(信息类状态码) 这类状态码表示临时响应,需要客户端继续处理请求。 100 Continue 服务器已收到请求的初始部分,客户端应继续发送剩余部分。 2xx(成功类状态码) 表示请求已成功被服务器接收、理解并处…...
webpack面试题
面试题:webpack介绍和简单使用 一、webpack(模块化打包工具)1. webpack是把项目当作一个整体,通过给定的一个主文件,webpack将从这个主文件开始找到你项目当中的所有依赖文件,使用loaders来处理它们&#x…...
