数据结构与算法分析c++第四版_数据结构与算法 - 时空复杂度分析

0d9bef77b2e950ab4b32338ece1efde8.png

这周主要总结了时间复杂度的学习,跟小伙伴们分享下,欢迎指正。

一、为何需要分析算法复杂度

挺多同学本科都学习过数据结构和算法这门课,但是有没有想过这门课到底是解决什么问题?科学家设计这些数据结构和算法是要干嘛?

其实,最终的目的只有一个:让我们写的代码在计算机上运行的速度更快,使用的内存更省!,可是如何才能知道我们写的代码使用多少运行时间和内存呢?这就需要分析算法时间复杂度和空间复杂度,只有学会分析这 2 者,才能知道我们设计的算法到底有没有性能的提升,不然你费了很大功夫想了一个算法,却发现运行速度慢如乌龟,得不偿失。

如果能够在运行算法之前就能知道算法大概的执行时间那就好了,而复杂度分析恰好可以解决这个问题!复杂度分析又分为 2 种:

1.1 运行后分析

这种就是写完算法直接放到机器上面跑,统计下到底用了多少时间和内存,但是这种方法有 2 个缺点:

  1. 测试结果依赖运行机器:性能强的机器当然需要的时间少
  2. 测试结果依赖于测试用的数据:比如对无序的数组和有序的数组排序的时间大不相同

1.2 运行前分析

那既然运行后分析有不可避免的缺点,有没有办法在纸上提前计算一下算法大概的执行时间和内存用量呢?当然有,就是今天的主角大 O 复杂度表示法!

二、大 O 复杂度表示法

我用一个例子来一步步解释大 O 复杂度表示法到底是什么意思:

int cal(int n) {int sum = 0;int i = 1;int j = 1;for (; i <= n; ++i) {j = 1;for (; j <= n; ++j) {sum = sum +  i * j;}}}

对 CPU 而言执行程序分为以下 3 步:

  1. 读如代码指令和数据
  2. 计算
  3. 输出数据

因为我们只是在运行前粗略地估计算法的运行时间,因此可以假设每行代码在 CPU 上运行的时间都相同,为 cputime,那么我们就可以直接计算出上面函数中所有代码执行的总时间(次数 x 单位时间),n 表示输入数据的规模:

  • 第 2、3、4 行:每行需要 1 个 cpu_time,共
  • 第 5、6 行:因为都是循环 n 次,所以需要
  • 第 7、8 行:因为内外一共有 2 层 n 次的循环,所以需要

那么总的运行时间 T(n) 为:

对于每个确定的算法,所有代码的执行次数一定,那么上面的 T(n) 与所有代码的执行次数

成正比关系,比例系数就是 CPU 执行每行代码的时间
cputime,因为可以把上式写成大 O 复杂度表示法

其中:

  • T(n):表示算法执行的总时间
  • f(n):表示算法总的执行次数,就是
  • O():表示 T(n)f(n) 的正比关系,也就是大 O 的由来

所以上面函数的总的执行时间又可以写成:

但是要注意:大 O 复杂度表示法并不是计算代码准确的运行时间,而是表示一种代码执行时间随着数据规模 n 增长的变化趋势,记住不是准确的时间,只是一种趋势而已,因为实际工作的算法可能需要接受很大量级的数据,通过分析算法运行时间与输入数据规模的变化趋势就能大概知道一个算法能不能在实际环境中很好的工作。

但是呢,上面的大 O 表示法还是不够简洁,比如当算法代码很多的时候,那我们是不是要在后面(或者前面)加上很多项:

这也不方便,因此大佬们又想了方法:** 只需要保留最大量级的运行次数即可!** 这是因为当输入数据规模很大,比如 100000, 1000000 等,常数项 3、一阶项 2n 等低阶的运行次数对最高次项

的影响并不大,所有代码的运行次数基本与最高阶
的运行次数保持一致。

因此我们只需要保留最高阶的运行次数就行,并且系数也可以去掉,因为我们表示的是变化趋势,常量系数并不影响变化趋势:

这样最终的大 O 复杂度表示法就成型了!这里之所以写的那么详细是因为这个复杂度分析真的非常重要,因为我们只把算法写出来还不够,我们还要能够分析算法的优劣,并且以后的工作中如果需要选择数据结构来完成指定的功能,你也必须要提前考虑自己选择的数据结构的运行时间,这是非常重要的,你做的不是玩具,而是要能给用户提供体验良好的产品。

三、时间复杂度分析

学完大 O 表示法后,分析时间空间复杂度就很容易了,就是把前面的推导过程在不同的算法上计算一下,不过常见的复杂度分析分为这 3 种。

3.1 只分析循环次数最多的一段代码

前面说过,大 O 表示法只需要关注最高阶运行次数,而在实际的代码中最高阶运行次数的代码往往是嵌套循环的最内层代码,也就是循环执行次数最多的一段代码,这种情况我们就只需要分析它就可以:

int cal(int n) {int sum = 0;int i = 1;for (; i <= n; ++i) {sum = sum + i;}return sum;}

可以发现执行次数最多的代码是 sum = sum + i,利用前面的大 O 分析法,很容易得出 T(n) = O(n)

3.2 加法法则

加入我们的程序有 2 个单层的 n 次 for 循环,还有一个 2 层的 n x n 的 for 循环,注意这里每个循环的数据规模是一样的,都是 n,必须保证这一点:

int cal(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;}

这种情况下我们只需要分别求出每个 for 的时间复杂度,然后加起来取最大的量级就行:

总的 T(n)

因为我们取最大量级,所以如果忽略数据规模 n 的特殊情况,我们可以取上面 3 个复杂度的最大值为最终结果:

上面算法最终的时间复杂度即为

,加法法则一般的公式如下,就是上面过程的一般情况,只不过用数学化的表示而已,很容易理解:

3.3 乘法法则

类比加法法则,乘法法则就很好理解,加法是相加,乘法就是多个复杂度相乘,体现在代码中就是有多个嵌套的循环调用,比如:

int cal(int n) {int ret = 0; int i = 1;for (; i < n; ++i) {ret = ret + f(i);} } int f(int n) {int sum = 0;int i = 1;for (; i < n; ++i) {sum = sum + i;} return sum;}

在 cal 函数的循环中每次都调用 f 函数来计算,所以总的时间复杂度是这两个函数的乘积:

注意不同与加法,乘法法则不是取最大的量级,而是直接幂次累加,要注意这点!

常用的时间复杂度

@(Google 王争)总结的几乎所有的复杂度如下图:

eea07d5c3c06e37f4c97d62b89ed0287.png

相信你应该听过 28 法则,20% 的技术能解决 80% 的问题,同样对复杂度分析,工作中常用就如下几个:

O(1)

代码中不含有循环、递归外的代码执行时间都为 O (1),记住 O (1) 并不是表示所有代码只执行一次,而是表示这些代码的执行次数不会随着输入数据规模 n 的变化而变化,即变化趋势是一个常量:

int i = 8;
int j = 6;
int sum = i + j;

不管有多少行上面这样的代码,他们的时间复杂度都是 O(1)

O(logn), O(nlogn)

这两种称为对数阶复杂度,在排序算法中比较常见,比如归并排序,快速排序的复杂度都是 O(nlogn),但是这种复杂度分析相对难一点,看个例子:

i=1;
while (i <= n)  {i = i * 2;
}

这段代码的复杂度就是分析 i = i * 2 的执行次数,只要求出执行次数就搞定了,学过等比数列的很容易看出,

,所以当:

求出:x = log2(n),则时间复杂度就是 O (log2n),依葫芦画瓢:

i = 1;
while (i <= n)  {i = i * 3;
}

同理 x = log3(n),复杂度为 O (log3n),但是如果有很多不同的代码是不是都要改变底数呢?并不用,因为对数有个换底公式,可以把一个对数换成指定的底数:

比如:

其中

是常数:

因为大 O 表示法可以省略系数,所以:

可以看出

求出的复杂度也可以表示为
,所以我们
统一忽略对数阶的底数,都表示为
,那么对于
就很好理解了,就是利用乘法法则把
复杂度的代码执行 n 次即可。

O(m + n), O(m * n)

这种复杂度有 2 个输入数据规模 m 和 n,这表示我们的算法接收 2 个数据的输入,但是因为数据规模不一定相同,所以不能简单的利用加法法则:

int cal(int m, int n) {int sum_1 = 0;int i = 1;for (; i < m; ++i) {sum_1 = sum_1 + i;}int sum_2 = 0;int j = 1;for (; j < n; ++j) {sum_2 = sum_2 + j;}return sum_1 + sum_2;
}

这种情况下,不能取最大的量级,因为数据规模不一样,我们只需要做复杂度的相加即可:T(n) = O(m + n),一般性的公式如下:

T1(m) + T2(n) = O[f(m) + g(n)]

但乘法规则仍然有效,如果两个嵌套循环的数据规模不同也成立:

for (int i = 0; i < m; i++)for (int j = 0; j < n; j++)...T1(m) * T2(n) = O[f(m) * g(n)]

四、空间复杂度分析

与时间复杂度类似,(渐进)空间复杂度表示的是算法的存储空间随着数据规模变化的趋势,空间复杂度分析比较容易:

void print(int n) {int i = 0;int[] a = new int[n];for (i; i <n; ++i) {a[i] = i * i;}for (i = n-1; i >= 0; --i) {print out a[i]}
}

比如:

  • 第二行:申请的 int 内存与数据规模 n 无关,空间复杂度为 O (1)
  • 第三行:申请的 int 数组内存与数据规模有关,规模越大申请的内存就越多,空间复杂度为 O (n)

所以根据大 O 表示法,最终的空间复杂度就是 O(n),常用的空间复杂有:

空间复杂度分析比较简单,只需要看看有没有与数据规模相关的内存申请操作即可,我们的重点还是放在时间复杂度分析,不管是面试还是考试,时间复杂度都是必定会问到的。

五、不同情况下的复杂度分析

除了以上各种情况的复杂度分析外,还需知道不同情况下的复杂度,主要分为 4 种情况:

  • 最好、最坏
  • 平均
  • 均摊

下面简单总结下,都挺好理解的。

5.1 最好、最坏情况时间复杂度

比如这个例子,在数组中查找元素 x 并返回索引:

// n 表示数组 array 的长度
int find(int[] array, int n, int x) {int i = 0;int pos = -1;for (; i < n; ++i) {if (array[i] == x) {pos = i;break;}}return pos;
}

假如要查找的元素 X 是数组的第一个元素,那么我们只需要循环一次就可以结束算法,此时的时间复杂度为 O (1),也即最好情况时间复杂度,但是假如我们要查找的元素 x 不在数组中,那么我们就需要循环 n 次结束算法,此时的时间复杂度为 O (n),对应的是最坏情况时间复杂度

5.2 平均情况时间复杂度

通常情况下,最好或者最坏时间复杂度情况发生的概率不大,所以为了表示一般情况下的复杂度,我们使用平均情况时间复杂度,这种情况需要简单的计算,不过很容易,还以上面的例子:

// n 表示数组 array 的长度
int find(int[] array, int n, int x) {int i = 0;int pos = -1;for (; i < n; ++i) {if (array[i] == x) {pos = i;break;}}return pos;
}

因为是平均情况所以可以做个假设,假设要查找的变量 x:

  • 在数组中的概率为 1/2
  • 不在数组中的概率也为 1/2
  • 出现在 0 - n-1 这 n 个数组位置的概率相同,即为 1/n

所以根据概率乘法:要查找的数据 x 出现在数组中的任意位置的概率为 1/2n,先出现在数组中的概率乘以任意位置的概率,然后我们就可以将每个元素被找到时要查找的次数对应概率相乘,最后再相加就得到算法的平均时间复杂度:

9bece1872cf3bd90d307307313c395dc.png

简单解释下:

  • 1 x 1/2n 表示数组第一个元素只需要循环查找一次
  • n x 1/2n 表示数组最后一个元素需要循环查找 n 次
  • n x 1/2 表示不在数组中的元素也需要查找 n 次,因为你必须把数组元素都遍历完后才能知道当前要查找的 x 不在数组中,那肯定就在数组外面喽,前面说过在数组外面的概率也是 1/2

那么最后的结果就是全部想加,因为可以省略系数,所以上面查找元素 x 算法的时间复杂度也是

。虽然存在这 3 种情况,但是实际工作中分析算法的时间复杂度时并不需要严格分析这 3 种不同情况。一般情况下,使用前面讲的复杂度分析方法得出复杂度即可,如果要详细推导的话,可以计算下平均时间复杂度。

5.3 均摊时间复杂度

这是一种特殊情况下的复杂度,并不是很常见,但还是了解下,它的主要思想是:把运行时间多的情况下的复杂度拆分,并均摊到运行时间少的情况下,看个例子:

// array表示一个长度为n的数组
// 代码中的array.length就等于n
int[] array = new int[n];
int count = 0;void insert(int val) {if (count == array.length) {int sum = 0;for (int i = 0; i < array.length; ++i) {sum = sum + array[i];}array[0] = sum;count = 1;}array[count] = val;++count;
}

这个算法实现如下功能,往数组中插入一个元素 val:

  1. 当数组满了后,对数组求和并把结果放到数组第一个元素的位置上,O (n)
  2. 当数组没满,直接插入元素 val 到空闲的位置上,O (1)

利用前面学习的复杂度分析可以很容易知道这两种情况的时间复杂度分别为:O(n)O(1),但是考虑实际运行情况,我们总是先把数组一个个位置存满 O (1),然后满了之后再执行求和的操作 O (n),并且这两种情况的发生是有规律的。

因此我们可以把比较耗时 O (n) 复杂度的求和操作的均摊到不太耗时的 O (1) 插入操作上,这样总体的时间复杂度就会变成 O (1),这就是均摊的思想,总结下均摊时间复杂度的应用场景:

  1. 大多数情况 1 是不耗时的操作
  2. 个别情况 2 是耗时操作
  3. 情况 1 和情况 2 的操作在时间和逻辑上都是连续的,有清晰的前后执行顺序,就类似上面这个例子

如果你工作中的算法满足这 3 个条件,那么可以尝试用均摊时间复杂度来分析,这种情况比较少见,但还是要知道有这样一种类型。

六、复杂度分析总结

今天学习的时空复杂度分析是数据结构和算法学习中非常重要的一环,只有学会分析时空复杂度,我们才能知道自己写的算法到底能够提升多快,如果不会分析复杂度,那只会盲目地选择数据结构,盲目地设计算法,时间复杂度就是我们设计算法的指标。

我们学习数据结构和算法的目的就是为了写出运行速度更快,存储用量更低的代码,如果都不会分析算法的执行时间和内存用量,那最后何谈学过这门课或者学会这门课呢?请务必重视复杂度分析,后面设计算法会经常用到。

OK,大家下个专题见:)

本文原创首发于微信公号「登龙」,分享机器学习、算法编程、Python、机器人技术等原创文章,扫码关注回复「1024」你懂的!

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

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

相关文章

java泰拉轴距_Java面向对象

Java面向对象什么是对象世界万物皆为对象&#xff0c;凡是能看得见摸得着的所以东西都叫对象。对象是由属性和行为组成&#xff0c;属性是对象所具有的特征&#xff0c;而行为是对象可以做的动作。>例如生活中常见的事物&#xff1a;汽车。汽车的品牌型号、颜色、轴距、车身…

python函数代码_如何显示Python函数的代码?

这有点老套&#xff0c;但是如果这是您经常要做的事情&#xff0c;您可以使用readline模块和函数修饰符。在class PrintableFunction(object):"""A class that contains a function and its start and end pointsin the readline history"""def …

mysql5.7 xtrabackup_MySQL 5.7 基于GTID建立运行主库的从库-xtrabackup+mysqldump

一.GTID innobackupex备份实现主从同步1)master备份innobackupex --defaults-file/etc/my.cnf --userroot --password123456 --parallel4 /backup2)拷贝到slave上&#xff0c;并prepare和copy backupinnobackupex --defaults-file/etc/my.cnf --apply-log --userroot --passwor…

swift for循环_Swift | 实战一个简单的素数计算器demo

Swift实战一个简单的素数计算器demo本期我们来介绍如何用storyboard来实现一个素数计算器demo&#xff0c;storyboard可以明确地知道界面上的组件与代码的关系&#xff0c;而且比起仅用代码写要方便不少。No.1制作一个简单的界面制作一个简单的界面我们首先要使用storyboard来绘…

java实验金额转换_java 数字金额转换中文金额

public static String digitUppercase(double n){String fraction[] {"角", "分"};String digit[] { "零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌&quo…

java文件传输连接方式_Java 学习笔记 网络编程 使用Socket传输文件 CS模式

Socket的简单认识Socket是一种面向连接的通信协议&#xff0c;Socket应用程序是一种C/S(Client端/Server端)结构的应用程序 Socket是两台机器间通信的端点。 Socket是连接运行在网络上的两个程序间的双向通讯端点。Socket通信原理Server服务端的输入流相当于Client客户端的输出…

如何给python升级_python升级后,如何给virtualenv里的python进行升级

我也碰到了这个问题&#xff0c;用brew升级python的时候忘记了备份&#xff0c;升级之后才发现pip也不能正常工作了&#xff0c;不过幸好找到了解决方法&#xff0c;重新安装几百兆的package实在是不能忍……先将虚拟环境中的部分文件删除cd rm .Pythonrm bin/pip{,2,2.7}rm bi…

创建线程的三种方法_Netty源码分析系列之NioEventLoop的创建与启动

前言前三篇文章分别分析了 Netty 服务端 channel 的初始化、注册以及绑定过程的源码&#xff0c;理论上这篇文章应该开始分析新连接接入过程的源码了&#xff0c;但是在看源码的过程中&#xff0c;发现有一个非常重要的组件&#xff1a;NioEventLoop&#xff0c;出现得非常频繁…

java彩票案例_java彩票例子

1.[代码]java彩票例子import java.util.HashSet; //自动过滤重复的集合import java.util.Random;public class LuckyTic {public static void main(String[] args) {// TODO Auto-generated method stubHashSet hs new HashSet();Random r new Random();int num r.nextInt()…

java正则表达式匹配xml标签_用正则表达式匹配HTML\XML等文件中的标签

测试用HTML源文件&#xff1a;View Code《完美世界&#xff1a;天界的召唤》缤纷圣诞总动员[ 中华网 1小时前]经过了平安夜和圣诞节&#xff0c;节日的气氛被推到了最高点&#xff01;《完美世界&#xff1a;天界的召唤》为玩家准备了精彩纷呈的圣诞节活动&#xff0c;而玩家也…

python cursor游标_第二十三天 python中游标的使用

游标(cursor)&#xff1a;系统为用户开通的一个数据缓冲区&#xff0c;用于存放SQL语句执行结果。用户使用的sql会逐一的在游标中获取记录&#xff0c;并赋值给主变量&#xff0c;交由Python进一步处理&#xff0c;一组主变量只能存放一条记录。例题&#xff1a;from T1 import…

python 列表比较不同物质的吸热能力_python列表里面根据一定的条件挑选元素

update:之前一版是错的&#xff0c;忽略了两层栈深还必须ticket、spce连续的要求换个解法&#xff0c;代码有些冗长#!/usr/bin/env python# -*- coding: utf-8 -*-def is_ticket(node):return node.startswith(ticket)def is_spec(node):return node.startswith(spec)def deal1…

java中arraycopy的用法_[jdk源码阅读系列]Java中System.arraycopy()的用法

本文转载&#xff0c;原文链接&#xff1a;3分钟了解Java中System.arraycopy的用法 - 伊万夫斯基 - 博客园 https://www.cnblogs.com/benjieqiang/p/11428832.html3分钟了解Java中System.arraycopy的用法System提供了一个静态方法arraycopy(),我们可以使用它来实现数组之间的复…

人工智能python小程序_Python:一个可以套路别人的python小程序

一个可以套路别人的python小程序~先简要介绍一下程序。程序是使用pycharm工具&#xff0c;python语言所写。程序包括客户端 client.py 和服务器端 server.py 两部分&#xff0c;利用了python中的socket包。咳咳&#xff0c;使用方法来了&#xff01;首先&#xff0c;你需要你和…

java spring hiberate_Java程序员:Spring Boot和Hibernate一起使用的技巧

Hibernate不需要多介绍&#xff0c;它是Java中最受欢迎的ORM。同样&#xff0c;Spring Boot是功能最强大且易于使用的框架。本文并不是描述一些关于Hibernate或Spring Boot的用法&#xff0c;因为有很多。相反&#xff0c;我们将研究同时使用它们时可能遇到的一些常见错误以及如…

postgresql 查询序列_RazorSQL for Mac(数据库工具查询) v9.0.9

RazorSQL Mac激活版是一款专门为mac用户推出的数据库管理软件&#xff0c;允许您从一个数据库工具查询&#xff0c;更新&#xff0c;导航和管理所有主要数据库&#xff01;软件特色RazorSQL 是一个非开源的功能非常强大数据库查询工具、SQL的编辑、数据库管理工具。支持通过 JD…

vsm特征提取java_文本特征提取方案汇总

文本特征提取方案汇总文本分析是机器学习算法的主要应用领域。但是&#xff0c;文本分析的原始数据无法直接丢给算法&#xff0c;这些原始数据是一组符号&#xff0c;因为大多数算法期望的输入是固定长度的数值特征向量而不是不同长度的文本文件。一、文本数据的表示模型​ 文本…

脚本启动显示查询频繁被服务器防御_又被CC攻击弄得心有余悸?莫怕!这里教你如何防御...

转自CSDN&#xff0c;博主&#xff1a;一只IT小小鸟。CC攻击原理HTTP Flood 俗称CC攻击(Challenge Collapsar)是DDOS(分布式拒绝服务)的一种&#xff0c;前身名为Fatboy攻击&#xff0c;也是一种常见的网站攻击方法。是针对 Web 服务在第七层协议发起的攻击。攻击者相较其他三层…

java 怎么获取形参名_获得方法形参名称列表 -- 哦也,搞定!!

JAVA获取类的方法的参数名 – 老话题,新方法!!折腾了一天,终于搞定了.测试了nutz所有的类,均读取正常!! 完美读取任何class的变量名信息! 呵呵,当前,前提是编译时含debug信息.无任何依赖,不需要asm,不要其他任何字节码工具,纯标准JDK API实现. 核心代码,仅一个方法,130行,哦也!…

robotframework安装_python3.9.0 + robotframework + selenium3 实例体验

在win10上安装python3.9.0robotframework中我们做了基本的使用robot framework的环境搭建&#xff0c;这一章主要通过一个简单的实例来体验下robot framework的使用方式、运行、报告和日志(非常漂亮的自动化测试报告噢&#xff01;)。首先我们打开RIDE&#xff0c;快捷键 ctrln…