时间复杂度和空间复杂度

跟随本教程学习数据结构,我们会给大家讲解一些与数据结构联系紧密的算法。

所谓算法,简单理解就是解决问题的方法(方案、思路)。通常情况下,一个问题的解决方法会有很多种,或者说解决这个问题的算法有很多种。

举个简单的例子,对数据集{2,4,5,1,3}做升序排序,解决这个问题的算法就有很多种,比如冒泡排序算法、快速排序算法、归并排序算法、希尔排序算法等。借助任何一种算法,都可以得到{1,2,3,4,5}升序序列。

同一个问题,使用不同的算法,虽然都可以解决问题,但有的算法执行效率高,有的算法执行效率低。就好比拧一个螺母,使用钳子和扳手都可以,但显然扳手拧螺母的效率更高。那么问题就出现了,怎样从众多算法中选择出“最好”的呢?

算法本身是不分“好坏”的,所谓“最好”的算法,指的是最适合当前场景的算法。通常情况下,挑选算法主要考虑两个方面,分别是:

  • 执行效率:根据算法编写出的程序,执行时间越短,效率就越高;
  • 占用的内存空间:不同算法编写出的程序,执行时占用的内存空间也不相同。如果实际场景中仅能使用少量的内存空间,就要优先选择占用空间最少的算法。

算法只是解决问题的思路,无法直接在计算机上执行。要想知道一个算法确切的执行时间和占用的内存大小,必须根据算法编写出可执行的程序。

当算法的数量较少时(比如 2、3 种),我们确实可以编写出各个算法对应的程序,逐个在机器上运行,记录下各自的执行时间和占用的内存大小,然后挑选出“最好”的算法。但是,如果算法的数量很多(比如 10 种,20 种),真机测试的方法将不再适用,因为将各个算法一一编写成程序的工作量是巨大的,得不偿失。

实际开发中,往往采用 “预先估值”的方法挑选算法。具体来讲,就是分析各个算法的实现过程(步骤),估算出它们各自的运行时间和占用的内存大小,就可以挑选出“最好”的算法。

我们习惯用「时间复杂度」预估算法的执行时间,用「空间复杂度」预估算法占用的内存大小。

时间复杂度

时间复杂度用来预估算法的执行时间。

以解决“求 n 的阶乘(n!)”为例,如下用伪代码描述出了解决此问题的一种算法:

输入 n             // 接收 n 的值
p <- 1             // p 的初值置为 1
for i<-1 to n:    // i 的值从 1 到 n,每次将 p*i 的值赋值给 p
    p <- p * i     
Print p            // 输出 p 的值

伪代码是一种介于自然语言和编程语言之间,专门用来描述算法的语言。伪代码没有固定的语法,本教程中我们用<-表示赋值过程,用Print xxx表示输出某个变量的值。

接下来,我就以这个算法为例,教大家如何计算一个算法的时间复杂度。

计算一个算法的时间复杂度,需要经过以下 3 个步骤。

1) 统计算法中各个步骤的执行次数

整个算法中共有 5 行伪指令,它们各自的执行次数分别是:

输入 n                 <- 执行 1 次
p <- 1                 <- 执行 1 次
for i<-1 to n:        <- i 的值从 1 遍历到 n,当 i 的值为 n+1 的时候退出循环,总共执行 n+1 次
    p <- p * i         <- i 从 1 到 n 的过程,共执行 n 次
Print p                <- 执行 1 次

所有伪指令执行次数的总和是2*n+4,显然它不是一个固定值,整个表达式的大小取决于 n 的值。

2*n+4可以直接作为算法执行时间的估值,但通常情况下,我们会做进一步地简化,并用规范的格式来表示一个算法的执行时间。

2) 简化算法的执行次数

通过统计各个算法中伪指令的执行次数,每个算法的运行时间都可以用类似 2*n+4、3*n^{2}+4*n+5 这样的表达式表示。那么,该如何比较各个表达式的大小呢?

我们可以尝试对每个表达式进行简化,简化的方法是:假设表达式中变量的值无限大,去除表达式中那些对结果影响较小的项。

以 3*n^{2}+4*n+5 为例,简化过程为:

  1. 当 n 无限大时,3*n^{2}+4*n 与 3*n^{2}+4*n+5 的值非常接近,是否加 5 对表达式的值影响不大,因此表达式可以简化为 3*n^{2}+4*n;
  2. 当 n 无限大时,3*n^{2} 的值要远远大于 4*n 的值,它们之间类似于 10000 和 1 之间的关系,因此是否加 4*n 对表达式最终的值影响不大,整个表达式可以简化为 3*n^{2}
  3. 当 n 无限大时,n^{2} 的值已经超级大,是否乘 3 对最终结果影响不大,整个表达式可以简化为  n^{2}

基于“n 值无限大”的思想,3*n^{2}+4*n+5 最终就简化成了 n^{2}。同样的道理,2*n+4 可以简化为 n。无论多么复杂的表达式,都可以采用这种方式进行简化。

3) 用大O记法表示算法的时间复杂度

除了用 n 外,一些人还可能会用 a、b、c 等字符作为表达式中的变量。为此,人们逐渐达成了一种共识,即都用 n 作为表达式中的变量,并采用大 O 记法表示算法的执行时间。

采用大 O 记法表示算法的执行时间,直接套用如下的格式即可:

O(频度)

频度指的就是简化后的表达式。

采用大 O 记法,2*n+4 可以用 O(n)表示,3*n^{2}+4*n+5 可以用 O(n2)表示。如果一个算法对应的表达式中没有变量(比如 10,100 等),则用 O(1)表示算法的执行时间。

如果一个算法的执行时间最终估算为 O(n),那么该算法的时间复杂度就是 O(n)。如下列举了常用的几种时间复杂度以及它们之间的大小关系:

O(1)< O(logn) < O(n) < O(n^{2}) < O(n^{3}) < O(2^{n})

O(1) 是最小的,对应的算法的执行时间最短,执行效率最高。

空间复杂度

空间复杂度用来估算一个算法执行时占用的内存大小。

执行过程中的程序,占用的内存空间主要包括:

  • 程序代码本身所占用的存储空间;
  • 如果需要输入输出数据,也会占用一定的存储空间;
  • 运行过程中,可能还需要临时申请更多的存储空间。

程序自身占用的存储空间取决于编写的代码量,如果想压缩这部分存储空间,要求我们在实现功能的同时尽可能编写足够短的代码。

程序运行过程中输入输出的数据,往往由要解决的问题而定,即便所用算法不同,程序输入输出所占用的存储空间也是相近的。

事实上,对算法的空间复杂度影响最大的,是程序运行过程中临时申请的内存空间。不同算法编写出的程序,运行时申请的临时存储空间通常会有较大不同。

因此,比较各个算法占用的内存大小,本质上比较的是它们执行过程中额外申请的内存空间的大小。举个简单的例子:

输入 n
A[1...n] <- {1...n}    <- 额外申请 n 个空间

根据 n 的值,算法执行时需要申请 n 个整数的内存空间,n 的值越大,额外申请的内存空间就越多。

和时间复杂度一样,空间复杂度也习惯用大 O 记法表示。空间复杂度的估算方法是:

  • 如果算法中额外申请的内存空间不受用户输入值的影响(是一个固定值),那么该算法的空间复杂度用 O(1) 表示;
  • 如果随着输入值 n 的增大,算法申请的存储空间成线性增长,则程序的空间复杂度用O(n)表示;
  • 如果随着输入值 n 的增大,程序申请的存储空间成 n^{2} 关系增长,则程序的空间复杂度用O(n^{2})表示;
  • 如果随着输入值 n 的增大,程序申请的存储空间成 n^{3} 关系增长,则程序的空间复杂度用O(n^{3})表示;

多数场景中,挑选 "好" 算法往往更注重的是时间复杂度,空间复杂度只要处于一个合理的范围即可。

当前阅读的是整套数据结构和算法教程中的一节内容,想系统学习数据结构和算法的读者,可以前往教程目录页面系统的学习,也可以猛击这里去我的个人门户网站学习,想购买教程的读者加我 q(834937624)领取教程的 PDF 电子版。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

数据结构和算法教程(C语言版)

创作不易,多多支持!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值