算法分析的正确姿势

719 查看

[本专题会对常见的数据结构及相应算法进行分析与总结,并会在每个系列的博文中提供几道相关的一线互联网企业面试/笔试题来巩固所学及帮助我们查漏补缺。项目地址:https://github.com/absfree/Algo。由于个人水平有限,叙述中难免存在不清晰准确的地方,希望大家可以指正,谢谢大家:)]

一、前言

在进一步学习数据结构与算法前,我们应该先掌握算法分析的一般方法。算法分析主要包括对算法的时空复杂度进行分析,但有些时候我们更关心算法的实际运行性能如何,此外,算法可视化是一项帮助我们理解算法实际执行过程的实用技能,在分析一些比较抽象的算法时,这项技能尤为实用。在在本篇博文中,我们首先会介绍如何通过设计实验来量化算法的实际运行性能,然后会介绍算法的时间复杂度的分析方法,我们还会介绍能够非常便捷的预测算法性能的倍率实验。当然,在文章的末尾,我们会一起来做几道一线互联网的相关面试/笔试题来巩固所学,达到学以致用。

二、算法分析的一般方法

1. 量化算法的实际运行性能

在介绍算法的时空复杂度分析方法前,我们先来介绍以下如何来量化算法的实际运行性能,这里我们选取的衡量算法性能的量化指标是它的实际运行时间。通常这个运行时间与算法要解决的问题规模相关,比如排序100万个数的时间通常要比排序10万个数的时间要长。所以我们在观察算法的运行时间时,还要同时考虑它所解决问题的规模,观察随着问题规模的增长,算法的实际运行时间时怎样增长的。这里我们采用算法(第4版) (豆瓣)一书中的例子,代码如下:

以上代码用到的StdIn和StdOut这两个类都在这里:https://github.com/absfree/Algo。我们可以看到,以上代码的功能是统计标准一个int[]数组中的所有和为0的三整数元组的数量。采用的算法十分直接,就是从头开始遍历数组,每次取三个数,若和为0,则计数加一,最后返回的计数值即为和为0的三元组的数量。这里我们采取含有整数数量分别为1000、2000、4000的3个文件(这些文件可以在上面的项目地址中找到),来对以上算法进行测试,观察它的运行时间随着问题规模的增长是怎样变化的。

测量一个过程的运行时间的一个直接的方法就是,在这个过程运行前后各获取一次当前时间,两者的差值即为这个过程的运行时间。当我们的过程本身需要的执行时间很短时间,这个测量方法可能会存在一些误差,但是我们可以通过执行多次这个过程再取平均数来减小以至可以忽略这个误差。下面我们来实际测量一下以上算法的运行时间,相关代码如下:

我们分别以1000、2000、4000个整数作为输入,得到的运行结果如下

我们从以上结果大概可你看到,当问题的规模变为原来的2倍时,实际运行时间大约变为原来的8倍。根据这个现象我们可以做出一个猜想:程序的运行时间关于问题规模N的函数关系式为T(N) = k*(n^3).

在这个关系式中,当n变为原来的2倍时,T(N)会变为原来的8倍。那么ThreeSum算法的运行时间与问题规模是否满足以上的函数关系呢?在介绍算法时间复杂度的相关内容后,我们会回过头来再看这个问题。

2. 算法的时间复杂度分析

(1)基本概念

关于算法的时间复杂度,这里我们先简单介绍下相关的三种符号记法:

  • 第一种叫Big O notation,它给出了运行时间的”渐进上界“,也就是算法在最坏情况下运行时间的上限。它的定义如下:对于f(n)和g(n),若存在常数c和N0,使得对于所有n > N0,都有 |f(n)| < c * g(n),则称f(n)为O(g(n)。
  • 第三种叫做Big Ω notation,它给出了运行时间的“渐进下界”,也就是算法在最坏情况下运行时间的下限。它的定义如下:对于f(n)和g(n),若存在常数c和N0,使得对于所有n > N0,都有|f(n)| > c * g(n),则称f(n)为Ω(g(n))。
  • 第三种叫Big Θ notation,它确定了运行时间的”渐进确界“。定义如下:对于f(n)和g(n),若存在常数c和N0,对于所有n> N0,都有|f(n)| = c * g(n),则称f(n)为Θ为Θ(g(n))。

我们在平常的算法分析中最常用到的是Big O notation。下面我们将介绍分析算法的时间复杂度的具体方法,若对Big O notation的概念还不是很了解,推荐大家看这篇文章:http://blog.jobbole.com/55184/。

(2)时间复杂度的分析方法

这部分我们将以上面的ThreeSum程序为例,来介绍一下算法时间复杂度的分析方法。为了方便阅读,这里再贴一下上面的程序: