我的编程空间,编程开发者的网络收藏夹
学习永远不晚

如何理解算法的复杂度

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

如何理解算法的复杂度

本篇内容主要讲解“如何理解算法的复杂度”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“如何理解算法的复杂度”吧!

1. Motivation - 为什么需要复杂度分析

事后统计法(也就是把代码运行一遍,通过统计、监控得到运行的时间和占用的内存大小)的测试结果非常依赖测试环境以及受数据规模的影响程度非常大。但是实际上更需要一个不用具体的测试数据就可以估计出算法的执行效率的方法。

2. 大 O 复杂度表示法

算法的执行效率简单来说就是算法的执行时间。比如下面这段代码的执行时间,假设每行代码执行的时间是一样的,都为  unit_time。在这个假设的基础之上,这段代码的总执行时间为 (2n + 2)* unit_time。

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

通过这个例子,可以看出总执行时间 T(n) 是与每行代码的执行次数成正比,即可以满足这个公式 T(n) = O(f(n)),其中 n  是数据规模的大小,f(n) 表示每行代码执行的总次,O() 表示一个函数,即 T(n) 与 f(n) 成正比。在这个例子中 T(n) =  O(2n+2),这种方式就被称为大 O 复杂度表示法。但是实际上,大 O  时间复杂度并不具体表示代码执行真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势,也叫做渐进时间复杂度,简称时间复杂度。那么,在 2n+2  这个式子中,系数 2 和 常数 2 并不左右增长趋势,比如它是线性,并不是会因为系数 2 或者常数 2  改变它线性增长的趋势,因此又可以写成T(n)=O(n)。又比如 T(n) = O(n^2),那么表示代码执行时间随数据规模 n 增长的变化趋势是 n  平方。下面这张图是不同时间复杂度,随数据规模增长的执行时间变化

如何理解算法的复杂度

3. 时间复杂度分析

如何对一段代码的时间复杂度进行分析呢?可以采用以下几种方法

  • 只关注循环次数最多的一段代码

因为大 O  复杂度表示法只是表示一种趋势,所以可以忽略掉公式中的常数项、低阶、系数等,只记录一个最大的量级就可以了。因此在分析一个算法、一段代码的复杂度的时候,只需要关注循环次数最多的那一段代码就行了。比如下面这段代码,时间复杂度是  O(n)

int cal(int n) {     int sum = 0;     int i = 1;     for (;i <= n; ++i) {         sum = sum + i;     }     return sum; }
  • 加法法则:总复杂度等于量级最大的那段代码复杂度

这个主要是省略掉大 O  复杂度中的低阶项。个人感觉这个方法跟上面的方法有些重合。比如下面这段代码中,可以按照循环分为三个段,第一个段中有个循环,但是循环次数是个常数项,对增长趋势无任何影响,因此时间复杂度是  O(1),第二段代码的时间复杂度是 O(n),第三个段代码的时间复杂度是 O(n^2)。这三个段中量级最大的那个时间复杂度也就是整段代码的时间复杂度。

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; }
  • 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积

比如下面这段代码中,f() 是一个循环操作,整个复杂度是 O(n),而 cal() 中的循环相当于外层,调用了 f(),假如把 f()  当成一个简单的操作的话,那么时间复杂度是 O(n),算上 f() 真实的复杂度之后,整个 cal() 的时间复杂度是 O(n)*O(n)=O(n*n) =  O(n^2)。

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; }

3.1. 常见时间复杂度

量阶表示
常量阶O(1)
对数阶O(logn)
线性阶O(n)
线性对数阶O(nlogn)
平方阶、立方阶、k次方阶O(n^2)、O(n^3)、O(n^k)
指数阶O(2^n)
阶乘阶O(n!)
其他阶O(m+n)、O(m*n)

下面针对上述的若干种时间复杂度进行阐述:

1.O(1)

O(1)是常量级时间复杂度的一种表示,只要代码的时间不随 n 的增大而增大,那么它的时间复杂也是  O(1)。一般情况下,只要算法中不存在循环语句、递归语句,即使有成千上万行的代码,其时间复杂度也是 O(1)。

2.O(logn)、O(nlogn)

对数时间复杂度往往比较难分析,比如下面这段代码中

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

如何理解算法的复杂度

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

如何理解算法的复杂度

O(nlogn) 的时间复杂度就相当于上面说到的“乘法法则”:一段代码的时间复杂度为O(logn) ,这段代码循环 n 次,时间复杂度就是  O(nlogn) 了。

3.O(m+n)、O(m*n)

这种情况下,代码的复杂度是由两个数据规模决定的。如下代码所示:

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; }

从这段代码中可以看出m 和 n 是两个数据规模,无法评估 m 和 n 的量级大小。因此,不能省略其中任何一个,所以就是 O(m+n) 了。

4.O(2^n)、O(n!)

在上述表格中列出的复杂度量级,可以粗略的分为两类:多项式量级和非多项式量级。其中非多项式量级只有这两个。非多项式量级的算法问题也叫做  NP(Non-Deterministic Ploynomial,非确定多项式)问题。当 n  越来越大时,非多项式量级算法的执行时间会急剧增加,求解问题的执行时间也会无限增长,所以是种很低效的算法。

3.2. 最好、最坏情况时间复杂度

比如下面这段代码中,是在数组中查找一个数据,但是并不是把整个数组都遍历一遍,因为有可能中途找到了就可以提前退出循环。那么,最好的情况是如果数组中第一个元素正好是要查找的变量  x ,时间复杂度就是 O(1)。最坏的情况是遍历了整个数组都没有找到想要的 x,那么时间复杂就成了 O(n)。因此 O(1)  就是这段代码的最好情况时间复杂度,也就是在最好的情况下,执行这段代码的时间复杂度。O(n) 就是这段代码的最坏情况时间复杂度。

// 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; }

3.3. 平均情况时间复杂度(加权平均时间复杂度或者期望时间复杂度)

最好和最坏情况时间复杂度都是极端情况发生的时间复杂度,并不常见。因此可以使用平均情况时间复杂度来表示。比如上面这段代码中查找 x  在数组中的位置有两种情况,一种是在数组中,另一种是不在数组中。在数组中又可以在数组中的 0~n-1 位置。假设在数组中和不在数组中的概率分别为  1/2,在数组中的 0~n-1 的位置概率都一样,为 1/(2 *n)。因此,上述这段的平均情况时间复杂度(或者叫加权平均时间复杂度、期望时间复杂度)为

如何理解算法的复杂度

假如使用如下公式计算复杂度的话,那么就相当于每种情况的发生概率都是 1/(n+1) 了,没有考虑每种的情况的不同概率,存在一定不准确性。

如何理解算法的复杂度

3.4. 均摊时间复杂度

均摊时间复杂度采用的是摊还分析法(平摊分析法)。就是把耗时多的操作,平摊到其他那些时间复杂度比较低的操作上。比如下面这段代码

// 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; }

这段代码想要实现的就是往一个数组中插入数据,如果数组满了的话,那么就求和之后的 sum  放到数组的第一个位置,之后继续将数据插入到数组中。通过分析可以发现,这段代码的最好情况时间复杂度是 O(1),最坏情况时间复杂度是 O(n),平均时间复杂度是  O(1)。

那么这段代码中,在大部分情况下,时间复杂度是 O(1),只有个别情况下,复杂度才是 O(n)。并且,O(1) 和 O(n) 出现的比较规律,一般一个  O(n) 执行之后,会出现 n-1 个 O(1) 的执行。针对这种情况,可以使用摊还分析法,就是把 O(n) 这个耗时多的时间复杂度均摊到接下来的 n-1 个  O(1) 的执行上,均摊下来的时间复杂度为 O(1),这个时间复杂度就是均摊时间复杂度。

那么均摊时间复杂度不怎么常见,常见的场景是:对一个数据结构进行一组连续操作,大部分情况下时间复杂度都很低,只有个别情况下时间复杂度比较高。而且这些操作之间存在前后连贯的时序关系,比如上面提到的先是一系列  O(1) 的时间复杂度操作,接下来是 O(n)  的时间复杂度操作。这个时候就可以采用摊还分析法将较高时间复杂度的那次操作的耗时平摊到其他时间复杂度比较低的操作上。

一般均摊时间复杂度等于最好情况时间复杂度。那么如何区别平均时间复杂度和均摊时间复杂度呢?我觉得看你使用哪种方法,假如使用摊还分析法算出来的时间复杂度就是均摊时间复杂度,使用加权方式、或者期望值计算方式算出来的时间复杂度就是平均时间复杂度。

4. 空间复杂度分析

空间复杂度分析方法很简单。时间复杂度的全称叫做渐进时间复杂度,表示算法的执行时间与数据规模之间的增长关系。那么空间复杂度全称叫做渐进空间复杂度,表示算法的存储空间与数据规模之间的增长关系。

比如下面这段代码中,首先 int i= 0; 申请一个空间存储变量,是常量可以忽略,int[] a = new int[n]; 申请了一个大小为 n 的  int 类型数组,剩下的代码都没有占用更多的空间,因此空间复杂度是 O(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]     } }

对于空间复杂度分析,其实比较简单,一般看变量声明时分配的空间大小即可。

4.1. 常用时间复杂度

量阶表示
常数阶O(1)
线性阶O(n)
平方阶O(n^2)

常用的空间复杂度就上面 3 种,O(nlogn)、O(logn)这样的对数阶复杂度一般都用不到。

5. 总结

回顾一下复杂度分析,总的来说时间复杂度的 motivation 是我们想要一个不用具体数据就可以估算出算法的执行效率的方法。而时间复杂度采用的是大 O  表示法,大 O 表示法其实描述的是一个增长趋势。比如 n^2 中,当 n 的值越来越大时候,O(n^2) 这个算法的执行时间是成平方增长的,而 O(n)  这个算法的执行时间是成直线型增长的,因此 O(n^2)  的时间复杂度是更高(见第一张图)。之后是几种常用的时间复杂度,平均时间复杂度、最好最坏时间复杂度,均摊时间复杂度(均摊这种思想在操作系统中有一定的体现:RR  调度算法中,在时间片大小选择上,有着类似的处理方式,因为 RR  是一个抢占式调度算法,当发生调度之后会发生进程的上下文切换,而进程的上下文切换是需要额外的时间成本,而这个时间成本会均摊到时间片上,当时间片很大时,显然均摊的效果不错,因此这个额外的时间成本影响会很小)

为什么说掌握时间复杂度是掌握了根本大法?去年上课的时候,记忆比较深刻的是老师好像在讲一个比较难的算法问题,然后从最简单、复杂度最高的解法开始讲起,然后跟带着我们一步一步分析每一块代码的时间复杂度,然后说这块的代码的时间复杂度是  O(n^2),我们能不能想办法把它给降下来的呢?然后就在那思考了怎么降了,一句一句代码看过去,画图等等,最终将时间复杂度降下来了。因此个人觉得掌握时间复杂度分析之后,掌握的是算法的分析方法,你可以分析出每段代码的复杂度,然后通过思考最终把相应代码的时间复杂度降下来。假如你复杂度分析掌握不熟,那么怎么降都不知道,那么算法的优化也就没了。

到此,相信大家对“如何理解算法的复杂度”有了更深的了解,不妨来实际操作一番吧!这里是编程网网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

如何理解算法的复杂度

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

怎么理解Java算法复杂度

本篇内容主要讲解“怎么理解Java算法复杂度”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“怎么理解Java算法复杂度”吧!大O符号衡量时间复杂度通常使用”大O符号“。什么是大O符号?我们需要先看
2023-06-02

web算法复杂度怎么理解

本篇内容介绍了“web算法复杂度怎么理解”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!算法学(Algorithmics)是设计和研究算法的科
2023-06-03

C语言中算法的时间复杂度和空间复杂度是什么

这篇文章给大家分享的是有关C语言中算法的时间复杂度和空间复杂度是什么的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。1.前言1.1 什么是数据结构?数据结构(Data Structure)是计算机存储、组织数据的方
2023-06-29

如何理解动态数组和时间复杂度

本篇内容主要讲解“如何理解动态数组和时间复杂度”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“如何理解动态数组和时间复杂度”吧!一、数组基础1.1 定义数组(Array)是一种线性表数据结构,它用
2023-06-15

MySQL中如何处理复杂计算

在MySQL中进行复杂计算通常需要使用函数、子查询、联合查询等方法来实现。以下是一些常用的处理复杂计算的方法:使用内置函数:MySQL提供了许多内置函数,如SUM、AVG、MAX、MIN等,可以用来进行基本的数学运算和统计计算。使用CASE
MySQL中如何处理复杂计算
2024-04-30

探索 PHP 数组去重算法的复杂度

php数组去重算法的复杂度:array_unique():o(n)array_flip() + array_keys():o(n)foreach 循环:o(n^2)探索 PHP 数组去重算法的复杂度简介在 PHP 中,数组去重是一个常见
探索 PHP 数组去重算法的复杂度
2024-04-28

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录