很多同学都觉得算法很难,难以入门,难以理解,更难以掌握和运用,其实归根溯源,我们可以把所有的问题都通过枚举法来解决,但是受困于「时间」「空间」的因素,有的时候并不能枚举所有的情况,所以需要通过精妙的算法设计避免枚举一些显而易见错误的情况。

那么既然说到了「时间」「空间」,这篇文章就跟大家聊一下算法设计过程中必须要考虑的时间复杂度空间复杂度

什么是时间复杂度和空间复杂度?

算法的时间复杂度和空间复杂度是对算法执行效率的分析,也是一个对算法的度量单位,目的是看算法实际是否可行,并且当同一个问题有多种解法时,可以进行时间和空间性能上的比较,以便从中挑选出较优算法。

衡量算法执行效率的方法有两种:

  1. 事后统计法
  2. 事前分析法

事后统计法需要先将算法实现,然后测算其真正的时间和空间开销。这种方法的缺陷很显然,一是必须把算法转换成可执行的程序,二是时空开销的测算结果依赖于计算机的软硬件等环境因素。

虽然我们都希望自己的算法更高效,但是每设计出来一个算法都写出程序来测算时空开销显然是不现实的,所有我们通常采用事前分析法,在编程前就尽量准确地估计程序的时空开销,通过数学分析和计算来估计算法的复杂度。

时间复杂度

算法的执行时间

先来看几个定义:
语句频度:一条语句的重复执行次数称为语句频度。
单位时间:由于语句的执行要由源程序经编译程序翻译成目标代码,目标代码经装配再执行,因此语句执行一次实际所需的具体时间跟机器软、硬件环境相关,所以设每条语句执行一次所需的时间均为单位时间。

一个算法的执行时间大致上等于其所有语句执行时间的总和,而语句的执行时间=语句频度×执行一次所需时间。

如果不考虑计算机的软硬件等环境因素,影响算法时间代价的最主要因素是问题规模,问题规模是算法求解问题输入量的多少,是问题大小的本质表示,一般用整数n表示。

问题规模n对不同的问题含义不同,例如:

  • 排序算法:n为参加排序的记录数
  • 矩阵运算:n为矩阵的阶数
  • 多项式运算:n为多项式的项数
  • 集合运算:n为集合中元素的个数
  • 树有关运算:n为树的结点个数
  • 图有关运算:n为图的顶点数或边数

显然n越大算法的执行时间越长。

例如:求两个n阶矩阵的乘积算法

矩阵乘法定义:

for(int i = 1; i < n + 1; i++) {  // 频度: n + 1 <思考:为什么是 n+1 而不是 n ?> for(int j = 1; j < n + 1; j++) { // 频度: n * (n + 1) c[i][j] = 0;    // 频度: n^2 for(int k = 1; k < n + 1; k++) {   // 频度: n^2 * (n + 1)c[i][j] = c[i][j] + a[i][k] * b[k][j];    // 频度: n^3}}
}

该算法中所有语句频度之和,是矩阵阶数n的函数,用f(n)表示:f(n)=2n3+3n2+2n+1f(n)=2n^3+3n^2+2n+1f(n)=2n3+3n2+2n+1。

算法的时间复杂度定义

通常,算法的执行时间是随问题规模增长而增长的,因此对算法的评价通常只需考虑其随问题规模增长的趋势。这种情况下,我们只需要考虑当问题规模充分大时,算法中基本语句的执行次数在渐进意义下的阶。

比如矩阵的乘积算法中,当n趋向于无穷大时,显然有:lim⁡n→∞f(n)n3=lim⁡n→∞2n3+3n2+2n+1n3=2\lim_{n\to \infty}\frac{f(n)}{n^{3}}=\lim_{n\to \infty}\frac{2n^3+3n^2+2n+1}{n^3}=2limn→∞​n3f(n)​=limn→∞​n32n3+3n2+2n+1​=2。

也就是说,当n充分大时,f(n)和n3之比是一个不等于零的常数,即f(n)和n3是同阶的,或者说f(n)和n3的数量级相同。

在这里,我们用“O”来表示数量级,记作T(n)=O(f(n))=O(n3),其中T(n)=O(f(n))就是算法的时间复杂度,它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,也叫算法的渐进时间复杂度,简称时间复杂度。

最好、最坏和平均时间复杂度

对于某些问题的算法,其基本语句的频度不仅仅与问题的规模相关,还依赖于其它因素。

例如:在一维数组a中顺序查找某个值等于e的元素,并返回其所在位置:

int find(int e) {for(int i = 0; i < n; i++) {if(a[i] == e) {return i + 1;}}return -1;
}

容易看出,此算法中基本语句的频度不仅与问题规模n有关,还与输入实例中数组a[i]的各元素值及e的取值有关。

假如运气爆棚,每次要找的元素e正好是数组中的第一个元素,那么不论数组的规模多大,基本语句的频度f(n)=1;假如运气极差,每次要找的元素e是数组的最后一个元素,则基本语句的频度f(n)=n。

对于一个算法来说,需要考虑各种可能出现的情况,以及每一种情况出现的概率,一般情况下,可假设待查找的元素在数组中所有位置上出现的可能性相同,则可取基本语句的频度在最好情况和最坏情况的平均值,即f(n)=n/2。

最坏时间复杂度:在最坏情况下,算法的时间复杂度
最好时间复杂度:在最好情况下,算法的时间复杂度
平均时间复杂度:所有可能输入实例在等概率出现的情况下,算法的期望运行时间

通常只讨论算法在最坏情况下的时间复杂度,确定算法执行时间的上界,以保证算法的运行时间不会比它更长。

时间复杂度分析举例

分析算法时间复杂度的基本方法为:找出所有语句中语句频度最大的那条语句作为基本语句,计算基本语句的频度得到问题规模n的某个函数f(n),取其数量级用符号“O”表示即可。

定理 1.1:若f(n)=amnm+am−1nm−1+...+a1n+a0f(n)=a_{m}n^{m}+a_{m-1}n^{m-1}+...+a_{1}n+a_{0}f(n)=am​nm+am−1​nm−1+...+a1​n+a0​是一个m次多项式,则T(n)=O(nm)。

定理 1.1 说明,在计算算法时间复杂度时,可以忽略所有低次幂和最高次幂的系数,这样可以简化算法分析,也体现出了增长率的含义。

下面举例说明一些常见的时间复杂度。

常数阶

例:获取程序支持的最大值。

const int MAXN = 1024;
int get_max() {return MAXN;
}

这个比较好理解,一共就一句话,没有循环,是常数时间,表示为 O(1)。

实际上,如果算法的执行时间是一个与问题规模n无关的常数,那么算法的时间复杂度为T(n)=O(1),称为常数阶。

对数阶

例:给定n(n<1000)个元素的有序数组a和整数v,求v在数组中对的下标,若不存在则返回-1。

这是一个常见的查找问题,我们可以用O(n)的算法遍历整个数组,然后去找v的值。当然,也有更快的办法,注意到题目中的条件,数组a是有序的,所以我们可以利用二分查找来实现。

int binary_search(int n, int a[], int v) {int l = 0, r = n - 1;while(l <= r) {mid = (l + r) >> 1;if(a[mid] == v) return mid;else if(a[mid] < v)r = mid + 1;elsel = mid + 1;}return -1;
}

由于我们每次都可以把搜索范围缩小一半,假设基本语句的频度为f(n),则有2f(n)<n,f(n)<log2n,所以算法的时间复杂度为T(n)=O(log2n),称为对数阶。

线性阶

例:给定n(n<1000)个元素ai,求其中奇数有多少个?

判断一个数是偶数还是奇数,只需要求它除上 2 的余数是 0 还是 1,那么我们把所有数都判断一遍,并且对符合条件的情况进行计数,最后返回这个计数器就是答案。

int count_odd(int n, int a[]) {int cnt = 0;for(int i = 0; i < n; ++i) {if(a[i] & 1)++cnt;}return cnt;
}

其中a & 1等价于a % 2

这个就是经典的线性时间复杂度O(n),称为线性阶。

线性对数阶

例:给定n(n<1000)个元素的有序数组a,求有多少个二元组(i, j),满足ai+aj=1024?其中(i<j)

枚举ai,然后在[i+1, n)范围内查找是否存在aj=1024-ai,存在则计数器+1,而这个查找的过程可以采用二分查找。

int count_odd(int n, int a[]) {int cnt = 0;for (int i = 0; i < n; ++i) {int l = i + 1, r = n - 1;while (l <= r) {mid = (l + r) >> 1;if(a[mid] == 1024 - a[i]) ++cnt;else if(a[mid] < v)r = mid + 1;elsel = mid + 1;}}return cnt;
}

该算法的时间复杂度为T(n)=O(nlog2n),称为线性对数阶。

平方阶

例:给定n(n<1000)个元素ai,求有多少个二元组(i, j),满足ai+aj是奇数?其中(i<j)

还是秉承枚举法的思想,需要两个变量i和j,枚举ai和aj,再对ai+aj进行奇偶性判断。

int count_odd_pair(int n, int a[]) {int cnt = 0;for(i = 0; i < n; ++i) {for(j = i+1; j < n; ++j) {if( (a[i] + a[j]) & 1) ++cnt;}}return cnt;
}

对循环语句只需要考虑循环体中语句的执行次数,以上程序段中频度最大的语句是if( (a[i] + a[j]) & 1) ++cnt;,其频度为f(n)=n(n−1)2f(n)=\frac{n(n-1)}{2}f(n)=2n(n−1)​,所以该算法的时间复杂度为T(n)=O(n2),称为平方阶。

多数情况下,当有若干个循环语句时,算法的时间复杂度是由最深层循环内的基本语句的频度f(n)决定的。

立方阶

例:给定n(n<1000)个元素ai,求有多少个三元组(i, j, k),满足ai+aj+ak是奇数?其中(i<j<k)

相信通过前面两个例子的分析,可以直接给出代码了。

int count_odd_triple(int n, int a[]) {int cnt = 0;for(i = 0; i < n; ++i) {for(j = i+1; j < n; ++j) {for(int k = j+1; k < n; ++k) {if( (a[i] + a[j] + a[k]) & 1 )++cnt;}}}return cnt;
}

该程序段中频度最大的语句是:if( (a[i] + a[j] + a[k]) & 1 ) ++cnt;,这条最深层循环内的基本语句的频度依赖于各层循环变量的取值,算法的时间复杂度为T(n)=O(n3),称为立方阶。


常见的时间复杂度按数量级递增排序:常数阶O(1)<对数阶O(log2n)<线性阶O(n)<线性对数阶O(nlog2n)<平方阶O(n2)<立方阶O(n3)<…<k次方阶O(nk)<指数阶O(2n)<阶乘阶O(n!)。

时间复杂度的计算

对于很多时间复杂度的题目,很多都是在做题时一眼就能看出程序的时间复杂度,但是无法规范地表述其推导过程。在这里总结此类题型的两种形式以及做题技巧。

  1. 循环主体中的变量参与循环条件的判断
    此类题应该找出主体语句中与T(n)成正比的循环变量,将之代入条件中进行计算。
int m = 5;
while ((m + 1) * (m + 1) < n) {m++;
}

m++的次数恰好与T(n)成正比,记t为该程序的执行次数并令t=m-5,有m=t+5,则(t+5+1)(t+5+1)<n,得t<n−6t<\sqrt{n}-6t<n​−6,即T(n)=O(n)T(n)=O(\sqrt{n})T(n)=O(n​)。

  1. 循环主体中的变量与循环条件无关
    此类题可采用数学归纳法或直接累计循环次数。多层循环时从内到外分析,忽略单步语句、条件判断语句,只关注主体语句的执行次数。此类问题又可分为递归程序和非递归程序。

    • 递归程序:使用公式进行递推

    • 非递归程序:直接累计次数

空间复杂度

算法的空间复杂度S(n)定义为该算法所需要的存储空间,它也是问题规模n的函数:S(n)=O(f(n))S(n)=O(f(n))S(n)=O(f(n))。

一个程序在机器上执行时,除了需要寄存本身所用的指令、常数、变量和输入数据外,还需要一些对数据进行操作和存储一些为实现计算所需信息的辅助空间。

其中,对于输入数据所占的具体存储量取决于问题本身,与算法无关,这样只需分析该算法在实现时所需的辅助空格键就可以了。

例:数组逆序,将一维数组a中的n个数逆序存放到原数组中。

算法1:

for(int i = 0; i < n; i++) {b[i] = a[n - i - 1];
}
for(int i = 0; i < n; i++) {a[i] = b[i];
}

算法1需要另外借助一个大小为n的辅助数组b,所以其空间复杂度为O(n)。

算法2:

for(int i = 0; i < n / 2; i++) {t = a[i];a[i] = a[n - i - 1];a[n - i - 1] = t;
}

算法2仅需要另外借助一个变量t,与问题规模n大小无关,所以其空间复杂度为O(1)。

算法原地工作是指算法所需的辅助空间为常量,即S(n)=O(1)。


关于「 算法时间/空间复杂度 」 的内容到这里就结束了。

Algorithm Master Road:算法的时间/空间复杂度相关推荐

  1. 数据结构与算法的时间空间复杂度

    提到数据结构与算法就不得不提时间复杂度和空间复杂度,本人看大部分文章介绍都比较晦涩难懂,就想着用简单的代码示例快速让你理解数据结构与算法的时间空间复杂度. 首先,时间复杂度表示的是使用某个数据结构或者 ...

  2. 算法及时间/空间复杂度的分析

    算法(Algorithm) 对特定问题求解步骤的一种描述,是为解决一个或一类问题给出的一个确定的.有限长的操作序列 算法的基本特征 ①输入:有零个或多个输入 ②输出:有一个或多个输出 ③有穷性:在执行 ...

  3. c++算法——算法章节-时间空间复杂度

    算法开章咯 这次是csp-j组算法 枚举法 hash vector 结构体 queue stack 贪心-简单 贪心区间 递归 二分 set map 二叉树 图的遍历-邻接矩阵 迷宫问题-dfs-深度 ...

  4. 算法设计与分析课程的时间空间复杂度

    算法设计与分析课程的时间空间复杂度: 总结 算法 时间复杂度 空间复杂度 说明 Hanoi $ O(2^n) $ $ O(n) $ 递归使用 会场安排问题 \(O(nlogn)\) \(O(n)\) ...

  5. 算法的时间和空间复杂度

    算法定义 算法由控制结构(顺序.分支和循环3种)和原操作(指固有数据类型的操作)构成的,则算法时间取决于两者的综合效果.为了便于比较同一个问题的不同算法,通常的做法是,从算法中选取一种对于所研究的问题 ...

  6. 排序算法 之四 分类、时间/空间复杂度、如何选择

    写在前面   现在网上关于排序算法的文档不计其数,为什么要写这篇文章呢?主要是因为一些算法虽然在平时有用到,但是从来没有细细整理过,没有个统一.整体的认识.写这篇文章一来是进行一下总结,二来趁机再系统 ...

  7. 简单排序算法时间空间复杂度分析及应用(4)-二分插入排序

    简单排序算法时间空间复杂度分析及应用(4)-二分插入排序 背景: 顾名思义,这个二分插入排序是直接插入排序的进化版,主要变化的地方就是在内循环部分,即外循环的循环节点在确定区域的位置查询方式由原来的直 ...

  8. 【算法】时间和空间复杂度

    文章目录 前言 一.时间复杂度 二.空间复杂度 三.常见的案例和示例 1. 线性查找(Linear Search) 2. 快速排序(Quick Sort) 3.动态规划(Dynamic Program ...

  9. 数据结构与算法二:时间/空间复杂度(complexity)

    从架构的角度来看,可扩展性是指更改 app 的难易程度.从数据库的角度来看,可伸缩性是指在数据库中保存或检索数据所需的时间快慢程度. 对于算法,可扩展性是指随着输入大小的增加,算法在执行时间和内存使用 ...

最新文章

  1. Dubbo基础专题——第四章(Dubbo整合Nacos分析细节点)
  2. go iris 连接 mysql 异步_go语言解决并发的方法有哪些?
  3. 用两个栈(C++)实现插入排序
  4. PAT甲级1005 Spell It Right :[C++题解]字符串处理
  5. 网页设计界面 电脑版设计
  6. Java黑皮书课后题第6章:*6.21(电话按键盘)国际标准的字母/数字匹配图如编程练习题4.15所示。编写一个测试程序,提示用户输入字符串形式的电话号码。程序将字母(大写或小写)翻译成数字
  7. Visual Media Server – 2 - 下载模块草图
  8. python批量转换图片格式_python批量将图片转换为JPEG格式
  9. 用python绘制玫瑰花的代码_python也能玩出玫瑰花!程序员的表白代码
  10. [振动力学]期中复习
  11. Android 网格视图GridView
  12. 如何在XSLT中将字符串转换为大写或小写形式
  13. micropython入门教程-我的MicroPython入门之路
  14. Raki的读paper小记:ALBERT: A LITE BERT FOR SELF-SUPERVISED LEARNING OF LANGUAGE REPRESENTATIONS
  15. windows中VMWare下安装Mac Os X 10.11踩坑记
  16. 小程序汉字转码以及倒计时
  17. 阿里云企业飞天会员是什么,如何申请?
  18. 万万没想到,“红孩儿” 竟然做了程序员,还是 CTO!
  19. Python编程中的常见语句
  20. 腾讯优图实验室贾佳亚:加入优图第一年 | 专访

热门文章

  1. 最后一次团队作业——总结
  2. 十种常用编程语言特点
  3. Error:java: JDK isn't specified for module 'bvisioncloud'
  4. vue里碰到 $refs 的问题
  5. hibernate 表关系映射详解之继承关系
  6. struts 权限控制
  7. .net利用程序集的GUID解决程序只能运行一次的问题
  8. matlab车辆贪心作业调度,贪心算法-区间调度-Interval Scheduling
  9. C语言学习之编写一个C程序,运行时输人abc三个值,输出其中值最大者。
  10. 9.找出1000以内的完数,所谓完数是指该数的各因子之和等于该数,如:6 = 1+2+3。