【算法】复杂度分析

第一章、如何分析代码的执行效率和资源消耗

        我们知道,数据结构和算法解决的是“快”和“省”的问题,也就是如何让代码运行得更快,一级如何让代码更节省计算机的存储空间。因此,执行效率是评价算法好坏的一个非常重要的指标。那么,如何衡量算法的执行效率尼?这里就要用到我们本节要讲的内容:时间复杂度分析和空间复杂度分析。

一、复杂度分析的意义

        我们把代码运行一遍,通过监控和统计手段,就能得到算法执行的时间和占用的内存大小,为什么还要做时间复杂度分析,空间复杂度分析呢?这种“纸上谈兵”似的分析方法比实实在在地运行一遍代码得到的数据更准确吗?

        实际上,这是两种不同的评估算法执行效率的方式。对于运行代码来统计复杂度的方法,很多有关数据结构和算法的图书还给它起了一个名字:事后统计法。这种统计方法看似可以给出非常精确的数值,但是却有非常大的局限性。

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²)。        

补充知识:

在数学中,我们经常会听到关于“高阶”、“低阶”、“常量”和“系数”的术语。让我来解释一下:

  1. 高阶(High-order):在多项式或函数中,高阶项是指指数较高的项。例如,在多项式 axn+bxn−1+cxn−2+…axn+bxn−1+cxn−2+… 中,axnaxn 就是高阶项,nn 是高阶项的指数。通常来说,当 nn 越大,该项的影响就越显著,因此被称为“高阶”。

  2. 低阶(Low-order):与高阶相对应,低阶项是指指数较低的项。在上面的多项式中,bxn−1bxn−1 和 cxn−2cxn−2 就是低阶项。这些项的影响相对较小,因为它们的指数较低。

  3. 常量(Constant):常量是没有包含任何变量的项,它们是数学表达式中的固定值。在多项式 axn+bxn−1+cxn−2+…+daxn+bxn−1+cxn−2+…+d 中,dd 就是常量项。

  4. 系数(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²)。

四、几种常见的时间复杂度量级

        虽然代码千差万别,但常见的时间复杂度量级并不多。简单总结一下,如图所示,这个涵盖了读者今后可以接触的绝大部分的时间复杂度量级。

计算数量级通常是对一个数的大小进行粗略估计,以确定它属于哪个数量级。这种估计可以通过以下步骤进行:

  1. 将数写成科学计数法:将数写成形如 a×10ba×10b 的形式,其中 1≤a<101≤a<10 是尾数,bb 是指数。例如,1234 可以写成 1.234×1031.234×103。

  2. 确定尾数 aa 的范围:确定尾数 aa 的范围。通常来说,aa 范围在 1 到 10 之间。

  3. 确定指数 bb 的值:指数 bb 表示了数值在数量级上的大小。例如,103103 表示数值在数量级上是千级别的。

  4. 确定数量级:根据指数 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成正比。

在实际应用中,除了考虑算法的时间复杂度之外,也需要考虑空间复杂度。对于内存有限的嵌入式系统或移动设备等场景,空间复杂度的控制非常重要,因为过高的空间复杂度会导致程序崩溃或无法运行。

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

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

相关文章

【Python笔记-设计模式】工厂模式

一、说明 (一) 解决问题 提供了一种方式&#xff0c;在不指定具体类将要创建的情况下&#xff0c;将类的实例化操作延迟到子类中完成。可以实现客户端代码与具体类实现之间的解耦&#xff0c;使得系统更加灵活、可扩展和可维护。 (二) 使用场景 希望复用现有对象来节省系统…

研学活动报名平台系统功能清单

中小学生社会实践活动、研学旅行等素质教育活动报名与管理平台&#xff0c;功能包含&#xff1a;活动分类&#xff0c;活动管理&#xff0c;在线报名缴费&#xff0c;扫码核销&#xff0c;会员特权体系&#xff0c;在线商城&#xff0c;研学互动。系统支持入驻老师自行创建研学…

「连载」边缘计算(十九)02-22:边缘部分源码(源码分析篇)

&#xff08;接上篇&#xff09; 从启动函数Start(&#xff09;中可以看到&#xff0c;其以go routine的方式启动很多后台处理服务&#xff0c;具体如下。 1&#xff09;初始化edged的kubeClient&#xff0c;具体如下所示。 // use self defined client to replace fake kube…

[hgame 2024 week3] crypto/pwn

第2周作完了不知道扔哪去了&#xff0c;先记录下第3周&#xff0c;因为官方WP已经出来&#xff0c;顺便把没出的题复现一下。最近的比赛都比较不错&#xff0c;相当于近期知识点的总结&#xff0c;有点心经的意思。 Crypto matrix_equation 题目很短&#xff0c;结了一个式子…

linux---防火墙拓展

目录 一、iptables 1.基本语法 2.四表五链——重点记忆 2.1四表 2.2五链 2.3总结 3.iptables选项示例 3.1 -Z 清空流量计数 3.2 -P 修改默认规则 3.3 -D 删除规则 3.4 -R 指定编号替换规则 4.白名单 5.通用匹配 6.示例 6.1添加回环网卡 6.2可以访问端口 6.3 主…

悄悄话花费的时间(C语言)

题目描述 给定一个二叉树&#xff0c;每个节点上站着一个人&#xff0c;节点数字表示父节点到该节点传递悄悄话需要花费的时间。 初始时&#xff0c;根节点所在位置的人有一个悄悄话想要传递给其他人&#xff0c;求二叉树所有节点上的人都接收到悄悄话花费的时间。 输入描述 …

基于java+springboot+vue实现的美食信息推荐系统(文末源码+Lw)23-170

1 摘 要 使用旧方法对美食信息推荐系统的信息进行系统化管理已经不再让人们信赖了&#xff0c;把现在的网络信息技术运用在美食信息推荐系统的管理上面可以解决许多信息管理上面的难题&#xff0c;比如处理数据时间很长&#xff0c;数据存在错误不能及时纠正等问题。这次开发…

第九节HarmonyOS 常用基础组件28-Select

1、描述 提供下拉选择菜单&#xff0c;可以让用户在多个选项之间选择。 2、接口 Select(options:Array<SelectOption>) 3、SelectOption对象说明 参数名 参数类型 必填 描述 value ResourceStr 是 下拉选项内容。 icon ResourceStr 否 下拉选项图标。 4…

在Mac上搭建MongoDB环境

最近工作中需要装MongoDB环境&#xff0c;搭建过程中遇到了一些问题&#xff0c;在这里记录一下安装MongoDB环境的方法以及问题的解决方法。有两种安装MongoDB的方法&#xff1a;brew安装和手动安装。 目录 使用Homebrew安装MongoDB 手动安装MongoDB&#xff08;不使用Homebr…

4G模块获取多个基站信息

获取多个基站信息&#xff0c;需要将其模块注网上不同的频段 以下使用ATQCFG"band" 配置频段 验证一 ATQENG"SERVINGCELL" 获取注网信息解析说明 在没锁频之前查询注网信息 [2024-02-23_11:22:41:108]ATCPIN? [2024-02-23_11:22:41:108]CPIN: R…

最简单方式把jar打包成Windows服务

废话 &#x1f622; 将JAR文件转化为Windows服务是一种高效且常见的Java应用部署策略。这种转变赋予了Java应用程序在Windows操作系统上以无界面后台服务模式运行的能力&#xff0c;从而实现了持续、稳定且可靠的功能提供。这种部署方式不仅提升了应用的可用性&#xff0c…

你真的了解nsight compute中的Roofline Charts吗?

此文章是之前看nsight-compute官网的时候做的笔记。因为这个记笔记的时候使用的文档记录下来会更直观好看&#xff0c;所以本次的笔记采用了截图的方式。中英文是方便沟通记忆&#xff0c;概念对齐。