系统的性能在很大程度上是由程序的算法所决定的。举一个例子,在大数据量的时候,折半查找就会比顺序查找快很多。
折半查找的例子
我们考虑一下从 100 万个数据中找出某一个特定数据的情况。假设检查 1 个数据需要花费 1 毫秒。如果使用从一头开始逐个检查所有数据的算法的话,因为从概率论来说要检查一半数据后才能找到要找的数据,所以可以计算出需要花费 50 万次 ×1 毫秒,也就是 500 秒。 接着,我们来考虑一下对于已经排好序的数据,一半一半地来查找的算法。首先,检查 100 万个数据的正中间的数据,假设数值是“50 万”。如果要查找的数据比 50 万小,就查找左半部分;如果比 50 万大,就查找右半部分的数据集,以此类推。这样,每检查 1 次就可以把查找范围缩小一半。虽然一开始有 100 万个查找对象,但检查 1 次后,查找对象就变成了 50 万个,接着变成 25 万个,再接着变成 12.5 万个。那么,什么时候查找对象会变为 1 个呢?通过计算我们可以知道大概是在第 20 次的时候(图 1.7)。检查 1 个数据花费 1 毫秒的话,总共就是 20 毫秒,与 500 秒相比就是一瞬间的事。读者可能会想:“当然不会使用从头开始依次检查这样没有效率的方法了!”但是计算机经常会被迫进行这样没有效率的处理,只是使用的人没有意识到而已。
可以忽略一些微小的系统开销
不过,一些一线程序员可能会想:“从头开始依次查找的方法和对半查找的方法相比,查找 1 个数据的时间应该是有差别的。从头开始查找的方法程序写起来比较简单。”的确,使用从头开始查找的方法,程序的编写比较轻松,处理也很简单,因此要查找 1 个数据花费的时间也很短。而使用对半查找的方法,需要记录自己检查到了哪个位置,还要计算下一步把哪部分数据对半,这是相对费事的。但其花费的时间是第一个方法的 1.5 倍?还是 2 倍?还是 3 倍?这里希望大家能有一种感觉,那就是这里所花费的时间从整体来看是很微小的,完全可以忽略。即使是 2 倍,那所花费的时间也只是 40 毫秒,3 倍的话也只是 60 毫秒。与从头开始查找所要花费的 500 秒比起来,差别依旧是很大的。 基于以上原因,在比较算法优劣的时候,会忽略掉一些微小的系统开销。我们应该关注的是随着数据个数的变化,所花费的时间会以怎样的曲线发生变化。因为有一些算法,在数据只有一两个的时候性能很好,但是当数据个数达到几千到几万的时候,性能会急剧下降。
算法的评价指标
将数据的个数用变量 n 来表示,让我们来比较一下直线 y = n 和曲线 y = n^2。可以发现 y = n2 的值会急剧变大(图 1.8)。即使是用 y = 2n 这条直线来与曲线 y = n^2 作比较,差别也依然很大。在这里,2n 的“2”对整体是不产生影响的,这就属于前面提到的“可以忽略的系统开销”。
计算机原则上是处理大量数据的东西,所以我们只关心当数据量变大时决定性能优劣的关键(Key)。这个关键就是“复杂度”(Order)。在前面列举的 y = n 或 y = 2n 中,其复杂度标记为 O(n)。而 O(1) 则表示不会受到数据量增加的影响。作为算法来说,这就非常好了。 打个比方,查找最小值的情况下,如果要从分散的数据的一端开始搜索,就需要查看所有的数据。换句话说,复杂度是 O(n)。但若数据是按顺序排列的,那第 1 个数据就是最小值,所以只要查看 1 次就完成了,复杂度就是O(1)。若数据个数是 5 个或 10 个这样的小数字,两者花费的时间差距可能只有几毫秒。但是若数据量达到了 100 万个会怎么样呢?这就会产生大约 1000 秒的差距。
通过复杂度来评价算法
我们就针对“查找数据”这种对计算机来说非常基础的操作,来通过复杂度判断一下算法的优劣。刚才已经介绍了将所有数据从头到尾查找一遍的方法。此外,还有一个非常有名的能提高数据查找效率的方法叫作“树”
树的根节点放置 1 个数据,接着将比它大的数据放在右边,比它小的数据放在左边,以此方式进行分类。对分类到左右两边的数据,以同样的方法进行分类。这样就会生成一个像“树”一样的结构。从树的根节点开始查找,直到找到目标数据为止,这一过程就是把数据对半分的操作。这种操作的复杂度标记为 O(logn)。logn 指的是把 n 除以 2 多少次后会变为 1。大体来说,它的复杂度处于 O(1) 和 O(n) 之间,用图来表示的话,可以看到即便数据变大,O(logn) 曲线也只是缓缓地上升。
如果你对本书敢兴趣,可以扫描购买:
如果你需要本书的视频教程,可以扫描学习, 或者点击这里学习
如果你喜欢我,可以关注我们的公众号: