算法简介(续)

150 阅读7分钟

下面来看看如何编写执行二分查找的Python代码。这里的代码示例使用了数组。如果你不熟 悉数组,也不用担心,下一章就会介绍。你只需知道,可将一系列元素存储在一系列相邻的桶(bucket),即数组中。这些桶从0开始编号:第一个桶的位置为#0,第二个桶为#1,第三个桶为#2, 以此类推。 函数binary_search接受一个有序数组和一个元素。如果指定的元素包含在数组中,这个 函数将返回其位置。你将跟踪要在其中查找的数组部分——开始时为整个数组。

1.png

你每次都检查中间的元素

2.png

如果猜的数字小了,就相应地修改low

3.png

如果猜的数字大了,就修改high。完整的代码如下。

4.png

1.2.2 运行时间

每次介绍算法时,我都将讨论其运行时间。一般而言,应选择效率最高的算 法,以最大限度地减少运行时间或占用空间。

回到前面的二分查找。使用它可节省多少时间呢?简单查找逐个地检查数 字,如果列表包含100个数字,最多需要猜100次。如果列表包含40亿个数字,最 多需要猜40亿次。换言之,最多需要猜测的次数与列表长度相同,这被称为线性 时间(linear time)。

二分查找则不同。如果列表包含100个元素,最多要猜7次;如果列表包含40亿个数字,最多 需猜32次。厉害吧?二分查找的运行时间为对数时间(或log时间)。下表总结了我们发现的情况。

5.png

1.3 大 O 表示法

大O表示法是一种特殊的表示法,指出了算法的速度有多快。谁在乎呢?实际上,你经常要 使用别人编写的算法,在这种情况下,知道这些算法的速度大有裨益。本节将介绍大O表示法是 什么,并使用它列出一些最常见的算法运行时间。

1.3.1 算法的运行时间以不同的速度增加 Bob要为NASA编写一个查找算法,这个算法在火箭即将登陆月球 前开始执行,帮助计算着陆地点。 这个示例表明,两种算法的运行时间呈现不同的增速。Bob需要做 出决定,是使用简单查找还是二分查找。使用的算法必须快速而准确。 一方面,二分查找的速度更快。Bob必须在10秒钟内找出着陆地点,否 则火箭将偏离方向。另一方面,简单查找算法编写起来更容易,因此出 现bug的可能性更小。Bob可不希望引导火箭着陆的代码中有bug!为确 保万无一失,Bob决定计算两种算法在列表包含100个元素的情况下需要 的时间。 假设检查一个元素需要1毫秒。使用简单查找时,Bob必须检查100个元素,因此需要100毫秒 才能查找完毕。而使用二分查找时,只需检查7个元素(log2100大约为7),因此需要7毫秒就能查 找完毕。然而,实际要查找的列表可能包含10亿个元素,在这种情况下,简单查找需要多长时间 呢?二分查找又需要多长时间呢?请务必找出这两个问题的答案,再接着往下读。

6.png

Bob使用包含10亿个元素的列表运行二分查找,运行时间为30毫秒(log21 000 000 000大约为 30)。他心里想,二分查找的速度大约为简单查找的15倍,因为列表包含100个元素时,简单查找 需要100毫秒,而二分查找需要7毫秒。因此,列表包含10亿个元素时,简单查找需要30 × 15 = 450 毫秒,完全符合在10秒内查找完毕的要求。Bob决定使用简单查找。这是正确的选择吗? 不是。实际上,Bob错了,而且错得离谱。列表包含10亿个元素时,简单查找需要10亿毫秒, 相当于11天!为什么会这样呢?因为二分查找和简单查找的运行时间的增速不同。

7.png

也就是说,随着元素数量的增加,二分查找需要的额外时间并不多, 而简单查找需要的额外时间却很多。因此,随着列表的增长,二分查找 的速度比简单查找快得多。Bob以为二分查找速度为简单查找的15倍, 这不对:列表包含10亿个元素时,为3300万倍。有鉴于此,仅知道算法 需要多长时间才能运行完毕还不够,还需知道运行时间如何随列表增长 而增加。这正是大O表示法的用武之地。 大O表示法指出了算法有多快。例如,假设列表包含n个元素。简 单查找需要检查每个元素,因此需要执行n次操作。使用大O表示法, 这个运行时间为O(n)。单位秒呢?没有——大O表示法指的并非以秒为单位的速度。大O表示法 让你能够比较操作数,它指出了算法运行时间的增速。 再来看一个例子。为检查长度为n的列表,二分查找需要执行log n次操作。使用大O表示法, 这个运行时间怎么表示呢?O(log n)。一般而言,大O表示法像下面这样。

8.png

这指出了算法需要执行的操作数。之所以称为大O表示法,是因为操作数前有个大O。这听 起来像笑话,但事实如此! 下面来看一些例子,看看你能否确定这些算法的运行时间。

1.3.2 理解不同的大 O 运行时间

下面的示例,你在家里使用纸和笔就能完成。假设你要画一个网格,它包含16个格子。

9.png

算法1

一种方法是以每次画一个的方式画16个格子。记住,大O表示法计算的是操作数。在这个示 例中,画一个格子是一次操作,需要画16个格子。如果每次画一个格子,需要执行多少次操作呢?

10.png

画16个格子需要16步。这种算法的运行时间是多少?

算法2

请尝试这种算法——将纸折起来。

11.png

在这个示例中,将纸对折一次就是一次操作。第一次对折相当于画了两个格子! 再折,再折,再折。

12.png

折4次后再打开,便得到了漂亮的网格!每折一次,格子数就翻倍,折4次就能得到16个格子!

13.png

你每折一次,绘制出的格子数都翻倍,因此4步就能“绘制”出16个格子。这种算法的运行 时间是多少呢?请搞清楚这两种算法的运行时间之后,再接着往下读。

答案如下:算法1的运行时间为O(n),算法2的运行时间为O(log n)。

1.3.3 大 O 表示法指出了最糟情况下的运行时间

假设你使用简单查找在电话簿中找人。你知道,简单查找的运行时间为O(n),这意味着在最 糟情况下,必须查看电话簿中的每个条目。如果要查找的是Adit——电话簿中的第一个人,一次 就能找到,无需查看每个条目。考虑到一次就找到了Adit,请问这种算法的运行时间是O(n)还是 O(1)呢?

简单查找的运行时间总是为O(n)。查找Adit时,一次就找到了,这是最佳的情形,但大O表 示法说的是最糟的情形。因此,你可以说,在最糟情况下,必须查看电话簿中的每个条目,对应 的运行时间为O(n)。这是一个保证——你知道简单查找的运行时间不可能超过O(n)。

说 明

除最糟情况下的运行时间外,还应考虑平均情况的运行时间,这很重要。最糟情况和平 均情况将在第4章讨论。