如何进行算法分析呢?
最简单方法是分别运行解决同一个问题的两个算法进行比较,但这样做法在很多时候并不理想。面对这样的困难迫使我们求助于数学工具,虽然我们不能对一个还没有完整实现的程序使用运行比较法,但却能通过数学分析了解程序性能的大致轮廓并预估改进版本的有效性。
大多数算法都有影响运行时间的主要参数 N,这里的 N 是所解决问题的大小的抽象度量,比如对于一个排序算法来说,N 是待排序元素的个数。我们的目标是尽可能地使用简单的数学公式,用 N 表达出程序的运行效率。
对于将要比较的两个算法,我们并不满足于简单地描述为“一个算法比另一个算法快”,而是希望能够通过数学函数直观地感受到二者的差异,具体来说,是希望知道“一个算法比另一个算法快多少”。
一些函数在算法分析中极为常见:
来看一下这些函数的增长曲线,如图 1 所示。
▲ 图 1 函数增长的曲线
以上函数能够帮助我们直观地理解算法的运行效率,让我们很容易区分出快速算法和慢速算法。大多数时候,我们都简单地把程序运行的时间称为“常数”、“线性”、“平方次”等。对于小规模的问题,算法的选择不那么重要,一旦问题达到一定规模,算法的优劣就会立马体现出来。代码 4-2 展示了当问题规模是 10、100、1000、10000、100000、1000000 时 lgN 、√N 、N、N lgN 、N² 、N³ 的增长规模。
▼代码 2 函数的增长规模 C4_2.py
01 import math
02
03 fun_list = ['lgN', 'sqrt(N)', 'N', 'NlgN', 'N^2', 'N^3'] # 函数列表
04 print(' ' * 10, end='')
05 for f in fun_list:
06 print('%-15s' % f, end='')
07 print('n', '-' * 100)
08
09 N_list = [10 ** n for n in range(7)] # 问题规模
10 for N in N_list: # 函数在不同问题规模下的增长
11 print('%-8s%-2s' % (N, '|'), end='')
12 print('%-15d' % round(math.log2(N)), end='')
13 print('%-15d' % round(math.sqrt(N)), end='')
14 print('%-15d' % N, end='')
15 print('%-15d' % round(N * math.log2(N)), end='')
16 print('%-15d' % N ** 2, end='')
17 print(N ** 3)
运行结果如图 4.2 所示。
图 2 告诉我们,该问题规模是 1 的时候,所有算法都同样有效,问题规模越大,不同复杂度的算法运行效率相差得越大。
2^N 增长得太过迅猛,作为一个另类单独列出。
▼ 代码 3 2^N 的增长
01 for N in range(10, 110, 10):
02 print('2**{0} = {1}'.format(N, 2 ** N))
运行结果如图 3 所示。
▲ 图 4.3 2^N 的增长
这些运行结果告诉我们,有些时候,选择正确的算法是解决问题的唯一途径。对于函数的输出结果来说,如果把 100 看作 1 秒,那么 10000 就是 100 秒,超过 1 分半。这意味着对于一个规模是 1000000 的问题来说,一个是 lgN 复杂度的算法可以立刻得出结果,√N 复杂度的算法耗时约 10 秒,N 复杂度的算法耗时将超过 2.7 小时,N^3 复杂度则需要 3 万多年!也许我们可以忍受一运行 10 秒或 2.7 小时的程序,但一定没法容忍有生之年看不到结果的程序。