用科学方法分析程序

计算机是复杂的,要准确回答“我的程序会运行多长时间?”、“为什么我的程序耗尽了所有内存?”之类的问题可能十分困难。

这时候可以用科学方法来分析程序。科学家用来理解自然世界的方法对于研究计算机程序的运行时间同样有效:

❏ 细致地观察真实世界的特点,通常还要有精确的测量;

❏ 根据观察结果提出假设模型;

❏ 根据模型预测未来的事件;

❏ 继续观察并核实预测的准确性;

❏ 如此反复直到确认预测和观察一致。

举例

比如,下面的程序会统计一个文件中所有和为 0 的三整数元组的数量。

统计所有和为 0 的三整数元组的数量
img

代码/图片中的 StdIn 和 StdOut 是《算法(第四版)》中的工具库,在这里功能上分别等价于 Java 中的 System.in 和 System.out(下同)。In.readInts(args[0]) 的作用是读取文件名 args[0] 对应文件中的所有整数。

多次运行程序并观察程序运行时间的变化,很快就能够提出一个符合直觉的假设(猜想)——程序的运行时间应该是随着问题规模(数组 a 的长度)的增长而变长。

根据该假设,我们可以预测——增大(或缩小)输入的规模,程序的运行时间会相应地变长(或变短)。

真的是这样吗?可以通过反复实验(运行该程序)来对该假设进行验证——预测与实验的结果相符,则认为该假设是可信的(至少暂时可信)。

经过进一步的观察,可发现该程序的运行时间和输入本身相对无关,它主要取决于问题规模,同样,我们也可以按科学方法对该假设进行验证。该假设对于许多程序都是成立的,但对于一些程序不成立,比如插入排序在待排序数组本身部分有序时更加高效,它的用时不仅受数组长度的影响,还受数组有序程度的影响。

另外,如果有几台计算机可供测试,那么基于多次观察我们也不难猜想并验证:程序在不同的计算机上的运行时间之比通常是一个常数。

对用时的增长分析

再进一步,我们可能想问,既然该程序的用时与输入本身相对无关,且随着问题规模的增长而变长,那么作为问题规模的一个函数,在问题规模为 N 时程序的运行时间是多久?

根据科学方法,为了回答该问题,我们可以用一批不同规模的数据对程序进行测试并计时,将问题规模与用时标在坐标平面上并用函数拟合,将拟合得到的函数作为假设,然后用该函数预测指定问题规模的用时并进行验证。

为方便进行统计和验证,我们用如表 1 所示的 Stopwatch 数据类型来计时。它在构造函数中用 Java 系统的 currentTimeMillis() 方法保存了当前时间,并在 elapsedTime() 方法被调用时再次调用该方法来计算得到对象创建以来经过的时间。

表 1 一种表示计时器的抽象数据类型
img

然后在 DoublingTest 中,我们为 ThreeSum 产生实验数据,反复对 TreeSum 程序进行测试并用 Stopwatch 进行计时。它会生成一系列随机输入数组,在每一步中将数组长度加倍,并打印出 ThreeSum.count() 处理每种输入规模所需的运行时间。

实验程序

public class DoublingTest
{
	public static double timeTrial(int N)
	{	// 为处理 N 个随机的六位整数的 ThreeSum.count() 计时
		int MAX = 1000000;
		int[] a = new int[N];
		for (int i = 0; i < N; i++)
			a[i] = StdRandom.uniform(-MAX, MAX);
		Stopwatch timer = new Stopwatch();
		int cnt = ThreeSum.count(a);
		return timer.elapsedTime();
	}
	public static void main(String[] args)
	{	// 打印运行时间的表格
		for (int N = 250; true; N += N)
		{	// 打印问题规模为 N 时程序的用时
			double time = timeTrial(N);
			StdOut.printf("%7d %5.1f\n", N, time);
		}
	}
}

实验结果

% java DoublingTest
250   0.0
500   0.0
1000   0.1
2000   0.8
4000   6.4
8000  51.1
...

之后,我们将实验得到的数据绘制到坐标平面,图 1 就是产生结果,使用的分别是标准比例尺和对数比例尺。其中 x 轴表示 N,y 轴表示程序的运行时间 T(N)。

图 1 实验数据(ThreeSum.count() 的运行时间)的分析
img

由对数的图像我们立即可以得到一个关于运行时间的猜想——因为数据和斜率为 3 的直线完全吻合。该直线的公式为(其中 a 为常数):

\[\lg(T(N)) = 3\lg N + \lg a \]

它等价于:

\[T(N)= aN^3 \]

这就是我们想要的运行时间关于输入规模 N 的函数。我们可以用其中一个数据点来解出 a 的值——例如,\(T(8000) = a8000^3\),可得 \(a = 9.98×10^{-11}\) ——因此我们就可以用以下公式预测 N 值较大时程序的运行时间:

\[T(N)=9.98×10^{-11}N^3 \]

之后,我们用这个函数来预测在指定数据规模下 TreeSum 程序何时将会结束,然后等待并验证结果是否正确。

物理学可分为实验物理和理论物理两大类,实验物理为理论物理提供观察资料并验证理论物理的猜想,理论物理分析从实验物理得到的资料并对其进行解释,进而建立新理论,它们是科学方法下不可分割的整体,“没有理论,实验结果是肤浅的;没有实验,理论则是不知正确性的想象”,前面的例子大多还属于实验的范围,需要有理论分析为其提供支撑。

总结自《算法(第四版)》1.4 节算法分析

posted @ 2022-02-24 15:37  Higurashi-kagome  阅读(119)  评论(0编辑  收藏  举报