手记

算法和数据结构-初级 | 第四课:算法复杂度(下)

程序 = 数据结构 + 算法

作者 谢恩铭 转载请注明出处
公众号「程序员联盟」(微信号:ProgrammerLeague )
原文:https://www.jianshu.com/p/3e5e987c7e05


《算法和数据结构-初级》全系列

内容简介


  1. 大 O 符号

  2. 时间复杂度和空间复杂度

  3. 最坏情况下的复杂度

  4. 第五课预告

1. 大 O 符号


上一课 算法和数据结构-初级 | 第三课:算法复杂度(上) 我们开始了算法复杂度的学习,这一课我们继续学习后半段。

我们已经看到,复杂度只考虑操作数目的一个数量级(忽略了其他的组分),这是一种近似。

为了表示这种近似,我们使用一个特定的符号,就是著名的 大 O 符号

大 O 符号(Big O notation),又称为渐进符号,是用于描述函数渐近行为的数学符号。更确切地说,它是用另一个(通常更简单的)函数来描述一个函数数量级的渐近上界。
在数学中,它一般用来刻画被截断的无穷级数尤其是渐近级数的剩余项。
在计算机科学中,它在分析算法复杂度的方面非常有用。
大 O 符号是由德国数论学家 保罗·巴赫曼(Paul Bachmann)在其 1892 年的著作《解析数论》(Analytische Zahlentheorie)首先引入的。而这个记号则是在另一位德国数论学家 艾德蒙·朗道(Edmund Landau)的著作中才推广的,因此它有时又称为 朗道符号(Landau Notation)。
代表“order of ...”(…阶)的大 O,最初是一个大写希腊字母“Ο”(Omicron),现今用的是大写拉丁字母“O”。
-- 摘自 百度百科

例如,小鸭子们去度假这个故事里,农夫 Oscar 的第一种算法有 N2 个操作,我们就说此算法的复杂度是 O(N2)。类似地,第二种更快的算法的复杂度是 O(N)。

大 O 符号有点像一个大圆形的袋子,可以把不同的操作数目整合在一起,使之具有一个同样的数量级。

例如,如果有三个算法,它们的操作数目分别为 N,5N + 7 和 N / 4,我们就都用 O(N) (读作 “N 的 大 O”。当然了,读法其实不是那么固定)来表示这三个算法的复杂度。

类似地,如果一个算法的操作数是(2 * N2 + 5 * N + 7),那么它的复杂度是 O(N2):我们忽略了 5 * N 和 7 这两项,因为它们与 2N2 相比数量级较小。随着 N 的增大,这两项的增长速率比 2N2 要慢,因此我们保留 2N2 即可,又因为常数乘法因子(这里是 2)不予考虑,因此记为 O(N2)。

我们说 f(N) 表示“N 的函数”(例如, f(N) = 2 * N2 + 5 * N + 7) ),那么 O(f(N)) 表示的是“大约有 f(N) 个操作的算法的复杂度”,这里的“大约”是非常关键的。

2. 时间复杂度和空间复杂度


下面我们来学习算法中常听到的“时间复杂度”和“空间复杂度”。

为什么我竟然想到了漫威里面的大反派灭霸的无限手套呢?上面有时间宝石和空间宝石这两颗无限宝石。
一定是因为我之前看了《复仇者联盟3:无限战争》的关系...

灭霸的无限手套上的六颗无限宝石

那么“时间复杂度”和“空间复杂度”这一对“活宝”到底是啥意思呢?且听我慢慢道来。

“在很久很久以前,宇宙中有 6 颗无限宝石,分别是时间宝石、空间宝石...”

读者:“小编,你快醒醒,讲正经的!”
我:“好,好,讲正经的,讲正经的~”

为了尽可能精确地表达算法的复杂度,我们可以做很多选择。

首先,我们选择输入条件的量化,例如通过变量 N(N 行 N 列小鸭子,N 个学生,N 架飞机,等)。当然了,不一定要用 N 这个变量名,我们可以选择其他变量名(比如 M,Z,X,Y 等),但更重要的是我们也可以不止用一个变量。

例如,如果我们的问题是要在一张纸上画画,那么我们可能会将算法的复杂度表达为画纸的长度 L 和宽度 W 的函数。同样地,如果农夫 Oscar 拥有比可用的池塘数目更多的小鸭子的行数,那么他可以将算法的复杂度表达为小鸭子的行数 N 和池塘数 P 的函数。

另一个重要的选择是要度量的操作的类型。到目前为止,我们其实只谈论了算法的效率或性能(就是算法快不快)。但是,程序员不仅对算法的执行时间感兴趣,他们也可能会度量许多其他特性,最常见的是内存消耗(Memory Consumption)。

算法的内存消耗也是度量算法复杂度的标准。例如,如果需要为一个输入大小为 N 的算法分配 N 千字节(KiloByte,一千个字节,简称 KB)的内存,则此算法的内存复杂度可以表示为 O(N)。

内存复杂度是和算法的内存消耗有关的复杂度,度量的并不是算法的效率,而是消耗/占用的内存空间大小,因此我们把它称为算法的空间复杂度(Space Complexity)。相对的,之前我们讨论的对于算法的执行速度(快不快)的度量是用的时间复杂度(Time Complexity)。

空间复杂度是对一个算法在运行过程中临时占用存储空间大小的度量,记做 S(N)=O(f(N))。
相对的,算法的时间复杂度就记为 T(N) = O(f(N))。
因为 S 是 Space(空间)的首字母,T 是 Time(时间)的首字母。

在计算算法的空间复杂度的时候,我们其实也不知道算法所消耗/占用的具体的内存大小(内存是以字节(Byte)为单位),我们计算的是算法所使用的(数据)结构的数量级。

比如说你使用 N 个大小为 N 的数组,那么其空间复杂度为 O(N2)。

例如,对于小鸭子们去度假的那个故事,可能农夫 Oscar 给他的每只小鸭子都起了一个英文名字。他随身携带着一份小鸭子的名字的表单,以免自己忘记。

小鸭子的名字表单

上面的表格是农夫 Oscar 用来记录小鸭子们的名字的表单的一个直观的表示:一共有 5 个名字(HARRY,JAMES,HENRY,EMILY,ALICE),分别对应 5 只小鸭子。表格里的每一行储存一个名字,每一行有 5 个格子(类似于数组的 5 个元素),5 x 5 = 25 个格子,一个格子里是一个英文字符。

如果联系到计算机的内存层面,N 只小鸭子需要 N 个数组来保存它们的名字,每个数组里是一只小鸭子的名字(都是英文字符),而数组的大小(这里是字符数)都统一为 N。所以这里的空间复杂度为 O(N2)。

有些时候,我们需要同时考虑算法的时间复杂度(执行速度)和空间复杂度(执行期间占用的内存空间的大小)。

一般在比较简单的情况下,我们对算法的空间复杂度没有那么关注。但对于更复杂的问题,算法的空间复杂度也许会引起更多的重视:例如,我们也许会通过牺牲一点执行速度来使用更少的内存;或者甚至通过增加算法的空间复杂度来提高执行速度,例如通过在表中存储已经计算好的结果(缓存(cache)的原理)。

对程序的约束越多,所需的信息就越精确。在计算机科学的某些领域,我们也会对算法的其他特征感兴趣。而这些特征中的某些也可以用算法的某种复杂度来度量。例如,大型计算机或嵌入式系统的程序员可能会考虑算法的功耗,以节省电量。

然而,在一般情况下,我们只关注算法的时间复杂度和空间复杂度,甚至主要关注时间复杂度。

3. 最坏情况下的复杂度


正如我们之前所说,算法执行的操作数目很明显取决于起始条件。

例如,下面是一个非常简单的算法,用于获知一个给定的值是否在值列表中(例如,“我是否已将鸡蛋加入我的购物清单?”):

为了获知一个给定的值是否在值列表中,我们可以这么做:
遍历整个列表,在找到给定值的时候即可停下,表示值在列表中;
如果我们已经遍历完整个列表,仍然没有找到给定值,那么说明给定的值不在值列表中。

想象一下,如果我们要查找的值不在列表中,并且列表里有 L 个元素。那么要确定这个值是否存在,算法就必须遍历一遍整个列表,将每个值与要查找的值进行比较,那将需要进行 L 次比较。因此,我们可以说算法具有 O(L) 的复杂度(很明显,这里考虑的是时间复杂度)。我们也可以说,此算法的时间复杂度是呈线性的(如果我们将输入列表的大小加倍,那么此算法将花费两倍的时间)。

但是,如果要查找的值位于列表的最开头,会怎么样呢?

例如,如果“鸡蛋”是我们的购物清单中的第一个元素,它会立即被注意到,我们将仅在进行一次操作后就停止遍历。在其他情况下,即使列表包含 3000 个元素,可能我们的搜索工作也会在 4 到 5 次操作后停止。

这就是 “最坏情况”(Worst Case)的概念发挥作用的地方:在计算算法的复杂度时,可以认为给定的输入对于我们的算法来说是处于“最坏的情况”。我们将计算需要最多操作(而不仅仅是一个或两个)的输入情况下的操作数,例如给定值不在列表里的情况。

从程序员的角度来看,这是一种安全性:计算出的复杂度处于“最坏情况”,因此他知道算法的表现只会更好。

就像网络安全领域的程序员会通过自问“最心怀恶意的用户可能会通过输入什么文本来入侵我的网站?”这样的问题来敦促自己提升应用程序的安全性一样,专注于算法研究的人也想知道“到底是算法中的哪个元素花了我的算法的大部分时间?”

这种方法可以度量所谓的“最坏情况下的复杂度”。

在本教程中,除非明确指出,我们只考虑算法在最坏情况下的复杂度。



作者:程序员联盟
链接:https://www.jianshu.com/p/3e5e987c7e05


0人推荐
随时随地看视频
慕课网APP