深入浅出算法复杂度分析
算法是计算机科学中最重要的部分,它们可以通过使用与语言和机器无关的方式进行研究。这意味着我们需要一种技术,使我们能够在不实现算法的情况下比较算法的效率。在算法分析中,两个最重要的工具是
- 计算的RAM模型。
- 最坏情况下复杂性的渐近分析。
本文将讨论算法的效率分析,以及常用的分析工具和方法。
0. Prerequisite
为了理解本文所写的内容,你需要掌握一些基本的高等数学知识。
- 极限的定义。
- 求极限的洛必达法则。
- 级数的定义与相关概念。
1. 算法的效率
一个算法主要有两种效率:时间效率和空间效率。时间效率,也称为时间复杂度,对应算法运行的速度。空间效率,也称为空间复杂度,是指算法根据其输入和输出所需的额外空间,或者说所需的内存单元数量。在计算机出现的早期,时间和空间等资源都是十分昂贵的。
半个多世纪依赖的技术创新使计算机的速度和内存大小提高了几个数量级。现在,算法所要求的额外空间通常不是人们关注的焦点。然而,算法时间效率重要性并不像空间那样降低。反而对于大多数问题,我们可以在时间上取得比在空间方面取得更令人满意的提升。因此,本文中,我们主要专注于算法的时间效率,当然但这里所提到的分析框架也适用于空间效率分析。
1.1. 衡量输入数据的大小
在讨论算法的效率之前,我们首先要明白,算法的效率会受到输入数据的影响。
几乎所有算法在较大的输入数据上运行的时间都更长。例如,对较大的数组进行排序、较大的矩阵乘法等都需要更长的时间。因此,选择一个合适的参数
在某些情况下,指示输入大小的参数的选择很重要。一个例子是计算两个
参数选择可能会受到算法的影响。比如,我们应该如何衡量拼写检查算法的输入大小?如果算法只是检查输每次入的单个字符,我们应该用字符集来衡量大小;如果算法通过处理单词来工作,那么我们应该在计算输入字符的数量。
对于解决诸如检查正整数
1.2. 运行时间与RAM模型
下一个问题涉及测量算法运行时间的单位。当然我们可以直接使用一些标准的时间测量单位,如秒或毫秒等来测量算法的运行时间。然而,这种方法有明显的缺点。
- 依赖于特定计算机的速度。
- 依赖于实现算法的程序和用于生成机器代码的编译器的质量
- 以及难以计时程序的实际运行时间。
考虑到这些问题,我们希望有一个不依赖于这些无关因素的指标。
如果我们想分析算法的运行时间,而又不通过具体的代码进行实验,我们可以直接对算法的伪代码(pseudo code)采取一种更具分析性的方法。
首先我们定义一组高级原始操作,这些操作并不依赖于实现算法时所使用的具体编程语言,也不依赖于任何特定的机器。仅仅在伪代码中使用,用于描述算法的各种操作。
- 为变量分配值。
- 调用方法。
- 执行算术操作。
- 比较两个数字。
- 索引数组。
- 对象引用。
- 从方法中返回。
具体而言,这些原始操作对应到特定机器上的特定低级指令,它们的执行时间取决于硬件和软件环境。我们并不是要确定每个原始操作的具体执行时间,而只是简单地计算算法执行的原始操作数,记为
计算算法的基本操作次数时,我们通常还要考虑输入的影响。如果在大小为
利用这个公式,我们可以合理估计算法的运行时间。比如,它可以帮助我们可以回答以下问题:
该算法在比快10倍的机器上运行的速度提升是多少倍?答案显然是10倍。
假设
,如果输入数据大小翻一番,算法将运行多久?答案是时间大约变为了四倍。
实际上,当
这种简单计算原始操作的方法称为 随机访问机 (Random Access Machine)计算模型。“随机访问”一词是指CPU拥有通过一个原始操作访问任意存储单元的能力。算法执行的原始操作数的确界直接反映了该算法在RAM模型中的运行时间。RAM模型还具有以下假设。
- 每一个原始操作仅需一个时间步
。 - 循环和子程序不应被视为简单的原始操作,而是由许多原始操作组成。显然,对100万个数字进行排序肯定比对10个数字进行排序所花费的时间多得多。循环或执行子程序所需的时间取决于循环迭代的次数和子程序的功能特性。
- 每次内存访问只需一个时间步。此外,我们假设拥有足够多的内存。RAM模型并不考虑数据是在主存中还是在磁盘上。
RAM是衡量计算性能的简单模型。也许太简单了。毕竟,在大多数处理器上执行乘法要比执行加法花费更多的时间,这违反了RAM模型的第一个假设;一些高级编译器对循环的展开优化和超线程技术很可能会违背第二个假设;而内存访问时间又因数据是位于缓存中还是磁盘上相去甚远。尽管如此,RAM仍然是了解算法在真实计算机上如何运行的绝佳模型。RAM模型的鲁棒性使我们能够以机器、语言无关的方式分析算法。
1.3. 最好情况、最坏情况、平均情况
虽然时空效率都以算法输入数据大小的函数来衡量,对于相同大小的输入,一些算法的效率可能会有很大差异。对于此类算法,我们需要区分最坏情况、平均情况和最坏情况的效率。
接下来考虑一个简单的顺序查找算法。在一个向量中搜寻一个目标值。若查找成功返回它的下标,查找失败则返回-1。
int search(vector<int> & A, int target) { |
1.3.1 最坏情况
显然,对具有相同规模
确定算法最坏情况下的效率的方法很简单:分析算法,看看哪种输入在所有可能的输入中能够使基本操作计数
1.3.2 最好情况
算法的最佳效率是在输入其大小为
最佳情况效率的分析远不如最坏情况效率的分析重要。但它也并不是完全无用。虽然我们不应该期望能够获得最佳情况下的输入,但对于某些算法,最佳情况下的性能可以帮助我们了解到一些接近最佳情况的输入的性能。
例如,对于插入排序算法,最佳情况下的输入是已有序的向量,那么该算法运行得非常快。而对于几乎已经有序的数组,最佳情况效率只会略微降低。因此,这种算法很可能是处理几乎已经有序数组的应用程序的首选方法。
最佳情况效率分析的另一个用处是,如果一个算法连最佳情况下的效率都无法令人满意,我们可以立即放弃它,无需再进一步分析。
1.3.3 平均情况
然而,无论是最坏情况分析还是最佳情况分析,都不能提供关于“随机”输入情况下的算法效率信息。平均情况分析可以做到这一点。为了分析算法的平均情况下的效率,我们首先必须对大小为
回到顺序搜索算法。我们提出两个概率假设。
搜索成功的概率等于
第一次匹配成功发生在列表第
个位置的概率对于所有 都是相同的。
在上述假设下(假设的有效性通常很难验证),我们可以计算出比较的平均次数
显然,分析平均情况下的效率比分析最坏情况和最佳情况的效率要困难得多。因为这样的分析往往需要大量的数学和概率知识,并且还需要我们事先知道输入数据所服从的概率分布。
分摊效率
还有一种效率称为分摊效率。它并不适用于算法单次运行,而是适用于在同一数据结构上执行的一系列操作。在某些情况下,一次操作的代价可能会很昂贵,但是,连续进行
具体的分摊分析方法并不是本文讨论的重点。感兴趣的话可以参考M. T. Goodrich的著作[7]。
1.4. 小结
时空效率都以算法输入大小的函数来衡量。
时间效率是通过计算算法基本操作的执行次数来衡量的。空间效率则是通过计算算法消耗的额外内存单元数量来衡量。
对于相同大小的输入,一些算法的效率可能会有很大差异。对于此类算法,我们需要区分最坏情况、平均情况和最好情况的效率。
我们主要关注随着输入大小不断增加,算法运行时间(消耗的额外内存单元)的增长速度。
2. 函数的增长性
我们注意到,实际上完全可以在不知道实际
所以,接下来我们将进入数学的世界,讨论分析函数的增长性的一般方法。
2.1. 渐进符号
如上一节所述,效率分析框架以算法基本操作数的增长顺序为中心,作为算法效率的主要指标。为了比较和排名这些增长顺序,计算机科学家借用了数学中的三个符号:
首先我们定义两种渐进函数
定义 2.1.1
定义2.1.2
在下面的讨论中,
2.1.1. Big-Oh Notation
直观上来讲,
尽管上述定义已经足够严谨,我们还是给出它的另一个等价定义。
这个定义通常读作“
例 2.1
2.1.2. Big-Omega Notation
第二个符号,
同样给出一个
也就是说,大
类似大
例 2.2
2.1.3. Big-Theta Notation
最后一个记号,
实际上,
类似地,读作“
2.1.4. Little-Oh Notation And Little-Omega Notation
除了上文提到的三种常用的渐进记号之外,还有两个不怎么常用的记号,分别是小
我们知道,大
回忆一下我们在数学上使用的小
注意,这里的
例 2.3 讨论
解:设
练习 2.1 试证明
练习 2.2 试证明
类似小
除此之外,还有一个不常用的符号
总结一下,
2.2. 进一步使用渐进符号
有时,对于一些比较复杂的函数,我们只关心它的主要部分。比如
假如我们使用小
例 2.4 试证明
练习 2.2 试证明
这些符号很有用,因为它们可以在不失数学严谨性和精确结果的情况下忽略函数中不重要的部分。
当涉及对数和指数时,我们应该认识到“指数差异”。例如,如果我们知道一个数量的值是
为了强调指数差异,如果一个函数
这种简便的表示方法确实方便了我们快速分辨
所以。只需判断
2.3. 渐进符号的运算性质
以下的运算性质都以大
使用好这些运算性质,可以在很多时候帮助我们简化计算。
例 试计算调和级数
2.4. 使用极限计算
虽然
例 2.5 比较
解 计算
解 计算
例 2.7 请比较
尝试计算它们之比的极限。注意到,
例 2.8 请比较以下函数的增长顺序。即求出满足
阶 | 函数 |
---|---|
常数 | |
这里给出一些重要的函数之间的比较过程。
与 。
与 。
与 。
与 。
2.5 多项式时间
在计算复杂性里,所谓多项式时间复杂度,就是指算法的复杂度可以控制在
为此,人们提出了P问题和NP问题。其中P问题就指的是可以在多项式时间内解决的问题,NP问题指的是可以在多项式时间内验证的问题。NP问题又包括了NPC问题,感兴趣的读者可以查阅计算理论相关的书籍。
接下来回到主题,继续讨论我们的时间复杂度。我们很关心的一个问题是,一个函数
例 2.9 函数
解:对于
也可以用反证法,假设
对于
3. 求解复杂度
根据算法策略的不同,算法可以分为迭代和递归两种。这两种策略有着不同的递推关系,下面我们讨论如何根据算法的递推关系求解算法的复杂度。
3.1. 减而治之
减而治之的问题的递推关系通常如下所示
这类的递推关系又称为线性递推关系,若
3.2. 分而治之
分而治之的问题通常具有以下形式的递推式。
请注意,主定理只是给出
要理解主定理并不难,首先回忆我们上述得出的
比较小,那么第一项占主导地位,于是 。- 当上述和式中的每一项都相互成比例时,那么
就是 与一个对数因子的乘积。 - 当
大于 时,第二项是以 为首项的递减的几何级数,所以 与 成正比。
下面我们简单证明一下第二种情况。设
例 3.1 设递推关系
解:
例 3.2 设递推关系
解:
例 3.3 设递推关系
解:
例 3.4 设递推关系
解:
例 3.5 设递推关系
解:这种情况比较棘手,不适用与主定理的情况。我们考虑进行变量替换。设
所以
3.3. 分摊分析
附录
附录中给出了一些有用的数学公式。
1. 对数函数
对于对数函数,通常有下面这些省略性的记号。
(在计算机科学中,我们默认底数为 ,在数学上则是默认为 ) (自然对数) (取幂) (复合运算)
除了以上的记号外,还有一个重要的记号是
请不要将它与数学上的高阶导数和函数的
由于只有在
我们仅简单证明第
例
例
2. Stiring公式
Stiring公式是逼近发散序列最著名的一个例子之一。它被用来逼近
4. 常用级数
此处介绍一些算法复杂度分析中常用的级数。其中,算术级数、幂方级数和几何级数是收敛的,调和级数和对数级数是发散的。
4.1 算术级数
算术级数与末项平方同阶。
4.2 幂方级数
幂方级数通常比末项高一阶。
4.3 几何级数
几何级数与末项同阶。
4.3 调和级数
调和级数是发散的,但是发散得非常缓慢。因此,可以使用一个公式逼近调和级数的值。
4.4 对数级数
对数级数同样是发散的,但我们可以通过Stiring公式来逼近它。
5. 范德蒙德卷积
范德蒙德卷积公式来源于组合数学中的选取问题。可以理解为从数量为
参考文献
[1] L. R. Vermani & S. Vermani. An Elementary Approach to Design and Analysis of Algorithms [M]. New Jersey: World Scientific, 2019. ISBN:978-1-78634-675-9.
[2] A. Levitin. Introduction to The Design & Analysis of Algorithms 3rd edition [M]. Pearson Education. 2012. ISBN:978-0-13-231681-1.
[3] M. H. Alsuwaiyel. Algorithms: Design Techniques and Analysis [M]. New Jersey: World Scientific. Ltd. 2016. ISBN: 9789814723640.
[4] T. H. Cormen & C. E. Leiserson & R. L. Rivest & C. Stein. Introduction to Algorithms 3rd edition [M]. The MIT Press. 2009. ISBN: 978-0-262-53305-8.
[5] N. Karumanchi. Data Structures And Algorithms Made Easy [M]. CareerMonk Publications. 2017. ISBN: 9788193245279.
[6] S. S. Skiena. The Algorithm Design Manual 2nd edition [M]. London: Springer - Verlag. 2008. ISBN: 978-1-84800-069-8.
[7] M. T. Goodrich & R. Tamassia. Algorithm Design And Application [M]. Jhon Willey & Sons. 2015. iSBN: 978-1-118-33591-8.
[8] R. Sedgewick. P. Flajolet. An Introduction to the Analysis of Algorithms 2nd edition [M]. Pearson Education. 2013. ISBN: 978-0-321-90575-8.
[9] D. Vrajitoru & W.Knight. Practical Analysis of Algorithms [M]. Switzerland: Springer International Publishing. 2014. ISBN: 978-3-319-09887-6.