实验研究
数据结构是组织和访问数据的一种系统化方式。
算法是在有限的时间里一步步执行某些任务的过程。
为了分辨哪些数据结构和算法是“优秀的”,我们一般用运行时间和空间利用来度量。
在Python 中,我们可以用time模块的 time()函数来记录一个算法的运行时间。(这个函数传递的是自新纪元1970年,基准时间后已经过去的秒数或分钟)
我们可以通过记录算法运行前的那一刻, 以及算法执行完毕后的那一刻,并计算他们的时间差来得到运行的时间。
from time import time
start_time = time()
my_algorithm()
end_time = time()
elapsed = end_time - start_time
print(elapsed)
但这样计算出来的时间是不准确的, 因为许多进程共享使用 CPU, 所有 elapsed 包含着一些其他作业执行的时间。 即使 使用相同的输入, 相同的算法,所得出来的时间可能没有保持一致性。更准确的方法应该是计算 my_algorithm() 使用的CPU周期的数量。
在Python中,包含一个更高级的模块 timeit, 它可以自动地多次重复 试验来评估差异。
而且 ,通过实验比较两个算法,有很多局限性(尤其是在测试生产环境中的代码时),比如:
- 很难直接比较两个算法的实验运行时间,除非试验在相同的硬件和软件环境中执行。
- 实现只有在有限的一组测试下才能完成, 因此忽略了不包括在实验中的输入的运行时间(这些输入可能是重要的)
- 为了在实验上执行算法来研究它的执行时间,算法必须完全实现。
最后一点,是实际中最严重的缺点。 因为在算法设计的初期,当考虑数据结构或算法的选择时,花费大量的时间实现一个显然低劣的算法是不明智的。
进一步的实验分析
我们的目标是开发一种分析算法效率的方法:
- 在软硬件环境独立的情况下,在某种程序上允许我们评价任意两个算法的相对效率。
- 通过研究不需要实现的高层次算法描述来执行算法。
- 考虑所有可能的输入。
计算原子操作
为了在没有执行实验室分析一个算法的执行时间,我们用一个高层次的算法描述直接进行分析(可以是真实的代码片段,也可以是独立于语言的伪代码)。 我们定义了一系列原子操作,如下所示:
- 给对象指定一个标识符
- 确定与这个标识符相关联的对象
- 执行算术运算(例如,两个数相加)
- 比较两个数的大小
- 通过索引访问Python列表的一个元素
- 调用函数(不包括函数内的操作执行)
- 从函数返回
一个原子操作相当于一个低级别指令,其执行时间是常数。
理想情况下,这些 是被硬件执行的基本操作类型。
我们并不会试着确定每一个原子操作的具体执行时间,而是简单的计算有多少原子操作被执行了,用数字 t 作为算法执行时间的度量。
操作的计数与特定计算机中真实的运行时间相关联,每个原子操作相当于固定数量的指令,并且该操作只有固定数量的原子操作。
算法执行的原子操作数 t 与算法的真实运行时间成正比。
随着输入函数的变化进行测量操作
为了获取一个算法运行时间的增长情况,我们把每一个算法和函数 ***f(n)***联系起来,其中把执行的原子操作的数量描述为输入大小为 n 的函数 ***f(n)***.
最坏情况输入研究
对于相同大小的输入,算法针对某些输入的运行速度比其他的更快。
因此,我们不妨把算法的运行时间表示为所有可能的相同大小输入的平均值的函数。
不幸的是,这样的评价情况分析是相当具有挑战性的。 它要求定义一组输入的概率分布,这通常是一个困难的工作。
最好的情况和最坏的情况通常运行时间不同
平均情况分析通常涉及复杂的概率理论。
因此,一般情况下我们都按照最坏情况来度量算法的时间。
最坏情况分析比平均情况分析容易很多,它只需要有识别最坏情况输入的能力,这通常是很简单的。另外,这个方法会到处更好的算法。
最坏情况的设计会使得算法更加健壮,这很像一个飞毛腿总是在斜坡上练习跑步。
以上内容是在阅读《数据结构与算法实现 Python语言实现》中的一些笔记。