算法是计算机科学中最重要的部分,它们可以通过使用与语言和机器无关的方式进行研究。这意味着我们需要一种技术,使我们能够在不实现算法的情况下比较算法的效率。在算法分析中,两个最重要的工具是

  1. 计算的RAM模型。
  2. 最坏情况下复杂性的渐近分析。

本文将讨论算法的效率分析,以及常用的分析工具和方法。

0. Prerequisite

为了理解本文所写的内容,你需要掌握一些基本的高等数学知识。

  • 极限的定义。
  • 求极限的洛必达法则。
  • 级数的定义与相关概念。

1. 算法的效率

一个算法主要有两种效率:时间效率和空间效率。时间效率,也称为时间复杂度,对应算法运行的速度。空间效率,也称为空间复杂度,是指算法根据其输入和输出所需的额外空间,或者说所需的内存单元数量。在计算机出现的早期,时间和空间等资源都是十分昂贵的。

半个多世纪依赖的技术创新使计算机的速度和内存大小提高了几个数量级。现在,算法所要求的额外空间通常不是人们关注的焦点。然而,算法时间效率重要性并不像空间那样降低。反而对于大多数问题,我们可以在时间上取得比在空间方面取得更令人满意的提升。因此,本文中,我们主要专注于算法的时间效率,当然但这里所提到的分析框架也适用于空间效率分析。

1.1. 衡量输入数据的大小

在讨论算法的效率之前,我们首先要明白,算法的效率会受到输入数据的影响。

几乎所有算法在较大的输入数据上运行的时间都更长。例如,对较大的数组进行排序、较大的矩阵乘法等都需要更长的时间。因此,选择一个合适的参数来指示算法输入的大小,对于研究算法的效率是非常必要的。在大多数情况下,这个参数的选择非常简单。例如,对于排序、搜索、查找列表最小元素以及处理列表的大多数其他问题,可以是列表的大小。有时,参数不止一个。比如图算法中的顶点数和边数

在某些情况下,指示输入大小的参数的选择很重要。一个例子是计算两个矩阵的乘积。此时对于输入的大小有两个选择,第一个是更常用的是矩阵维度。另一个是矩阵中元素的总数(后者更笼统,因为它适用于非方阵)。虽然这两个参数可以相互转换,但算法效率的计算结果将因我们使用这两种参数中的哪一种而异。

参数选择可能会受到算法的影响。比如,我们应该如何衡量拼写检查算法的输入大小?如果算法只是检查输每次入的单个字符,我们应该用字符集来衡量大小;如果算法通过处理单词来工作,那么我们应该在计算输入字符的数量。

对于解决诸如检查正整数的素性等问题的算法,输入数据的大小又是另一种情况。这里的输入只是一个数字,正是这个数字的大小决定了输入的大小。在这种情况下,通常应当用的二进制表示中的位数来衡量输入大小。

1.2. 运行时间与RAM模型

下一个问题涉及测量算法运行时间的单位。当然我们可以直接使用一些标准的时间测量单位,如秒或毫秒等来测量算法的运行时间。然而,这种方法有明显的缺点。

  • 依赖于特定计算机的速度。
  • 依赖于实现算法的程序和用于生成机器代码的编译器的质量
  • 以及难以计时程序的实际运行时间。

考虑到这些问题,我们希望有一个不依赖于这些无关因素的指标。

如果我们想分析算法的运行时间,而又不通过具体的代码进行实验,我们可以直接对算法的伪代码(pseudo code)采取一种更具分析性的方法。

首先我们定义一组高级原始操作,这些操作并不依赖于实现算法时所使用的具体编程语言,也不依赖于任何特定的机器。仅仅在伪代码中使用,用于描述算法的各种操作。

  • 为变量分配值。
  • 调用方法。
  • 执行算术操作。
  • 比较两个数字。
  • 索引数组。
  • 对象引用。
  • 从方法中返回。

具体而言,这些原始操作对应到特定机器上的特定低级指令,它们的执行时间取决于硬件和软件环境。我们并不是要确定每个原始操作的具体执行时间,而只是简单地计算算法执行的原始操作数,记为,并将它作为算法运行时间的一个高层次估计。

计算算法的基本操作次数时,我们通常还要考虑输入的影响。如果在大小为的输入上执行算法,那么执行次数记为。假设是每个原始操作需要花费的时间,那么算法在特定机器上的运行时间就可以用以下公式表示。 运行时间将与算法在特定的硬件和软件环境中的实际运行时间相关。因为每个原始操作在不同的机器上都将有不同的恒定执行时间,并且根据算法的确定性,每一个算法只会执行固定数量的原始操作。注意,这里有一个隐含的假设,在一台机器上,不同原始操作的执行时间应当近似。如此一来,算法执行的原始操作数将与该算法的实际运行时间成正比。

利用这个公式,我们可以合理估计算法的运行时间。比如,它可以帮助我们可以回答以下问题:

  1. 该算法在比快10倍的机器上运行的速度提升是多少倍?答案显然是10倍。

  2. 假设,如果输入数据大小翻一番,算法将运行多久?答案是时间大约变为了四倍。

实际上,当很大时,总有

因此

这种简单计算原始操作的方法称为 随机访问机 (Random Access Machine)计算模型。“随机访问”一词是指CPU拥有通过一个原始操作访问任意存储单元的能力。算法执行的原始操作数的确界直接反映了该算法在RAM模型中的运行时间。RAM模型还具有以下假设。

  • 每一个原始操作仅需一个时间步
  • 循环和子程序不应被视为简单的原始操作,而是由许多原始操作组成。显然,对100万个数字进行排序肯定比对10个数字进行排序所花费的时间多得多。循环或执行子程序所需的时间取决于循环迭代的次数和子程序的功能特性。
  • 每次内存访问只需一个时间步。此外,我们假设拥有足够多的内存。RAM模型并不考虑数据是在主存中还是在磁盘上。

RAM是衡量计算性能的简单模型。也许太简单了。毕竟,在大多数处理器上执行乘法要比执行加法花费更多的时间,这违反了RAM模型的第一个假设;一些高级编译器对循环的展开优化和超线程技术很可能会违背第二个假设;而内存访问时间又因数据是位于缓存中还是磁盘上相去甚远。尽管如此,RAM仍然是了解算法在真实计算机上如何运行的绝佳模型。RAM模型的鲁棒性使我们能够以机器、语言无关的方式分析算法。

1.3. 最好情况、最坏情况、平均情况

虽然时空效率都以算法输入数据大小的函数来衡量,对于相同大小的输入,一些算法的效率可能会有很大差异。对于此类算法,我们需要区分最坏情况、平均情况和最坏情况的效率。

接下来考虑一个简单的顺序查找算法。在一个向量中搜寻一个目标值。若查找成功返回它的下标,查找失败则返回-1。

int search(vector<int> & A, int target) {
int i = 0;
while(i < A.size()) {
if (A[i++] == target) return i;
}
return -1;
}

1.3.1 最坏情况

显然,对具有相同规模的不同向量,该算法的运行时间可能大不相同。在最坏的情况下,当没有匹配元素或匹配元素恰好在向量的末尾时,该算法的比较次数将是所有大小为的可能输入中最多的:

确定算法最坏情况下的效率的方法很简单:分析算法,看看哪种输入在所有可能的输入中能够使基本操作计数达到最大值,然后计算这个最坏情况下的值。显然,最坏情况分析确定了算法的运行时间上界(Upper Bound),提供了关于算法效率的非常重要的信息。它保证对于任何大小为的输入实例,运行时间不会超过。因此,我们的算法分析主要关注最坏情况下的效率分析。

1.3.2 最好情况

算法的最佳效率是在输入其大小为时,该算法在所有可能输入中运行最快的情况。换言之,最好情况分析确定了一个算法运行时间的下界(Lower Bound)。我们可以对最好情况效率进行如下分析。首先,确定在所有可能的输入中,使得计数最小的输入类型,然后计算这个最好输入情况下的值。例如,顺序搜索的最佳情况是第一个元素就是我们要找的目标元素,此时,算法的

最佳情况效率的分析远不如最坏情况效率的分析重要。但它也并不是完全无用。虽然我们不应该期望能够获得最佳情况下的输入,但对于某些算法,最佳情况下的性能可以帮助我们了解到一些接近最佳情况的输入的性能。

例如,对于插入排序算法,最佳情况下的输入是已有序的向量,那么该算法运行得非常快。而对于几乎已经有序的数组,最佳情况效率只会略微降低。因此,这种算法很可能是处理几乎已经有序数组的应用程序的首选方法。

最佳情况效率分析的另一个用处是,如果一个算法连最佳情况下的效率都无法令人满意,我们可以立即放弃它,无需再进一步分析。

1.3.3 平均情况

然而,无论是最坏情况分析还是最佳情况分析,都不能提供关于“随机”输入情况下的算法效率信息。平均情况分析可以做到这一点。为了分析算法的平均情况下的效率,我们首先必须对大小为的所有可能输入做出一些假设。

回到顺序搜索算法。我们提出两个概率假设。

  1. 搜索成功的概率等于

  2. 第一次匹配成功发生在列表第个位置的概率对于所有都是相同的。

在上述假设下(假设的有效性通常很难验证),我们可以计算出比较的平均次数,如下所示。在成功搜索的情况下,在向量的第个位置匹配成功的概率为,此时进行的比较的次数为。在搜索失败的情况下,比较的次数为,概率为 。因此的期望计算如下。 根据这个期望的公式,我们可以得出一些相当合理的结论。比如,如果(搜索一定成功),则通过顺序搜索进行的关键比较的平均次数为,也就是说,该算法将平均搜索大约一半的元素。如果(搜索一定失败),则比较次数的期望将为,因为算法将搜索所有的元素。

显然,分析平均情况下的效率比分析最坏情况和最佳情况的效率要困难得多。因为这样的分析往往需要大量的数学和概率知识,并且还需要我们事先知道输入数据所服从的概率分布。

三种情况下算法效率增长示意图
三种情况下算法效率增长示意图

分摊效率

还有一种效率称为分摊效率。它并不适用于算法单次运行,而是适用于在同一数据结构上执行的一系列操作。在某些情况下,一次操作的代价可能会很昂贵,但是,连续进行次这样的操作的总时间总是比单次操作的时间乘以要少得多。因此,我们可以在整个操作序列中“摊销”单次操作的高成本。这种复杂的方法是由美国计算机科学家罗伯特·塔扬(Robert Tarjan)发现的,他在研究一些经典二叉搜索树的变体,如伸展树的连续插入操作时使用了这种方法。

具体的分摊分析方法并不是本文讨论的重点。感兴趣的话可以参考M. T. Goodrich的著作[7]

1.4. 小结

  • 时空效率都以算法输入大小的函数来衡量。

  • 时间效率是通过计算算法基本操作的执行次数来衡量的。空间效率则是通过计算算法消耗的额外内存单元数量来衡量。

  • 对于相同大小的输入,一些算法的效率可能会有很大差异。对于此类算法,我们需要区分最坏情况、平均情况和最好情况的效率。

  • 我们主要关注随着输入大小不断增加,算法运行时间(消耗的额外内存单元)的增长速度。

2. 函数的增长性

我们注意到,实际上完全可以在不知道实际的值的情况下回答上文提出的关于运行时间的第二个问题,因为它已经被抵消了。另外,计数公式中的常数乘法因子也被抵消了。因此,我们在算法分析中常常忽略这些常数因子,随着输入大小的不断增长,对运行时间起主导作用的主要是函数增长的阶。

所以,接下来我们将进入数学的世界,讨论分析函数的增长性的一般方法。

2.1. 渐进符号

如上一节所述,效率分析框架以算法基本操作数的增长顺序为中心,作为算法效率的主要指标。为了比较和排名这些增长顺序,计算机科学家借用了数学中的三个符号:。渐进一词则是来源于数学上函数的渐近线。

首先我们定义两种渐进函数

定义 2.1.1 (或)中的函数称为渐近非负函数,若存在,对任意的,总有

定义2.1.2 (或)中的函数称为渐近正函数,若存在,对任意的,总有

在下面的讨论中,可以是在自然数集上定义的任何函数。但在算法分析中,我们的计数函数总是正的,所以有关的函数都将加上绝对值。将表示算法的运行时间(通常与基本操作计数成正比),是一个用来和进行比较的简单函数。

2.1.1. Big-Oh Notation

直观上来讲,是一个函数集合,集合中所有元素的增长速度小于等于的增长速度(当趋向于无穷时,增速之比为常数倍)。因此,举几个例子,以下断言都是正确的:

定义 2.2.1 是一个函数集合,满足存在实数和正整数,对任意的,总有。若,则称,记作

尽管上述定义已经足够严谨,我们还是给出它的另一个等价定义。

定义 2.2.2 是一个函数集合,满足 ,则称,记作

这个定义通常读作“是大 ”或者“阶”。大记号给我们提供了一个当充分大时,渐进意义下的上界 (Asymptotic Upper Bound)。

例 2.1 同样正确✅。我们通常希望表示的是更紧的上界。

2.1.2. Big-Omega Notation

第二个符号,,表示与具有相同或更高增长速度的所有函数的集合(当趋向于无穷时,增速之比为常数倍)。

定义 2.3.1 是一个函数集合,满足存在实数和正整数,对任意的,总有。若,则称,记作

同样给出一个集合的等价定义。

定义 2.3.2 是一个函数集合,满足 ,则称。记作

也就是说,大记号与大记号表示的意义恰好相反。假设,当n充分大时,根据大记号的定义,有。我们将常数除到左边,得到,根据大记号的定义,这意味着

类似大记号的读法,大记号通常读作“是大 ”。大记号表示当充分大时,渐进意义下的下界 (Asymptotic Lower Bound)。

例 2.2 同样正确✅。但请注意,我们通常希望表示的是更紧的下界。

2.1.3. Big-Theta Notation

最后一个记号,也是一个函数的集合,集合中函数的增长速度与相同当趋向于无穷时,增速之比为常数倍)。

定义 2.4.1 我们称,当且仅当。记作

实际上,就是的交集。并且我们仍然能够通过另一种方式判断是否是

定义 2.4.2 是一个函数集合,满足 ,则称。记作

类似地,读作“是大 ”。大记号允许我们说两个函数在渐近意义上是相等的,他们可以相差最高可达一个常数因子。我们考虑以下一些这些符号的例子。

2.1.4. Little-Oh Notation And Little-Omega Notation

除了上文提到的三种常用的渐进记号之外,还有两个不怎么常用的记号,分别是小和小

我们知道,大记号是用于描述一个函数的渐进上界的,然而这个上界并不一定是严格的上界,可能是渐进紧 (Asymptotically Tight)的上界。比如,。当趋向于无穷时,两个函数值之比为。此时,是一个渐进紧的上界,因为你不能再找到一个比更小,而又渐进意义上大于等于的函数了。从数学上来说,他们是同阶的无穷大,增速相同。而就是一个严格的宽松的上界。

回忆一下我们在数学上使用的小记号,表示的是高阶无穷小 ,也就是 这里的表示的就是比严格的高阶的的无穷小。我们从数学中借用小符号,得出以下定义。

定义 2.5 是一个函数集合,满足任意的实数存在整数,当时,总有。即

注意,这里的表示的是所有比低阶的函数集合,也就是是一个严格的上界,或者说非渐进紧 (Non-Asymptotically Tight)的上界。比如,

例 2.3 讨论的关系。

解:设 对任意实数,总有,使得,即无界。故,从而

练习 2.1 试证明

练习 2.2 试证明

类似小记号,小记号表示一个函数是严格的、非渐进紧的下界。

定义 2.6 是一个函数集合,满足任意的实数存在整数,当时,总有。即

除此之外,还有一个不常用的符号,表示两个函数相似。

定义 2.7 当且仅当

总结一下,提供了表达上限的方法(是更严格的上界),提供了一种表达渐近等价性的方法。

2.2. 进一步使用渐进符号

有时,对于一些比较复杂的函数,我们只关心它的主要部分。比如,我们只关心对的增长起到主导作用的。在引入了渐进符号后,我们便可以使用渐进符号简化该函数的表达式了。

假如我们使用小记号,那么可以表示为: 请不要忘记,小记号表示的是严格的上界。如果我们想使用的渐进紧的上界,那么可以使用大记号,如此一来,就可以表示为:

定义 2.8 Big-Oh近似 我们说,以表明我们可以通过计算来近似,并且误差将以的常数倍为界。正如在大符号的定义中提到的一样,所涉及的常数是不确定的,但我们通常假设它是不大合理的。如下文所述,我们通常使用的符号。

定义 2.9 Little-Oh近似 一个更强的表示是,同样表明我们可以通过计算来近似。但随着的变大,误差与相比将变得越来越小。这个非特定函数与下降率有关,它在数值上永远不会很大。

定义 2.10 Similarity近似 用于表达最弱的非平凡近似:

例 2.4 试证明

练习 2.2 试证明

这些符号很有用,因为它们可以在不失数学严谨性和精确结果的情况下忽略函数中不重要的部分。

当涉及对数和指数时,我们应该认识到“指数差异”。例如,如果我们知道一个数量的值是,那么我们可以合理地确定,当大到甚至时,在真实值的百分之几或几千分之一,那么我们不值得计算的系数,甚至可以直接将其缩减到以内。同样,的渐近估计显而易见。

为了强调指数差异,如果一个函数小于的任何负数次幂,即,那么我们称它是指数级小(Exponentially Small)的。典型的指数小的量是(你能证明吗?)。

这种简便的表示方法确实方便了我们快速分辨的主要部分,由此引申出另一个问题,假如我们已知,如何判断这种简化表示的等式是否真的成立?比如,等式是否成立?

定义 2.11 我们记当且仅当

所以。只需判断是否正确即可。 故该等式成立。

2.3. 渐进符号的运算性质

以下的运算性质都以大记号为例,但这些性质对我们已经引入的所有的渐进符号都成立。

定理 2.1

  1. 传递性,那么
  2. 数乘
  3. 乘法
  4. 加法

使用好这些运算性质,可以在很多时候帮助我们简化计算。

试计算调和级数的平方的渐进函数,要求给出误差项。

2.4. 使用极限计算

虽然的正式定义对于证明其抽象性质不可或缺,但它们很少直接用于比较两个特定函数的增长速度。根据渐进符号的定义,我们发现一个更便捷的方法是利用极限作为判断两个函数增长顺序的公式。但请注意,两个函数比值的极限存在是个充分非必要条件。

定理 2.2

例 2.5 比较的增长顺序。

解 计算之比的极限。如果读者有一定高等数学基础的话,应该立即知道它们之比的极限为 例 2.6 比较的增长顺序。

解 计算之比的极限。

例 2.7 请比较的增长顺序。

尝试计算它们之比的极限。注意到,,于是由复合函数的极限可得:

例 2.8 请比较以下函数的增长顺序。即求出满足的一种排列。请将你的排列按等价类形式划分,即在相同的类别中,当且仅当 解:顺序如下表所示,增长速度由上至下变大。

函数
常数

这里给出一些重要的函数之间的比较过程。

2.5 多项式时间

在计算复杂性里,所谓多项式时间复杂度,就是指算法的复杂度可以控制在以内,其中是任意常数。限制于当前计算机硬件的速度,如果一个算法是指数级复杂度,那么意味着它的效率是及其地下的,人们往往对那些多项式时间复杂度的算法更感兴趣。

为此,人们提出了P问题和NP问题。其中P问题就指的是可以在多项式时间内解决的问题,NP问题指的是可以在多项式时间内验证的问题。NP问题又包括了NPC问题,感兴趣的读者可以查阅计算理论相关的书籍。

接下来回到主题,继续讨论我们的时间复杂度。我们很关心的一个问题是,一个函数,不论它的表达式多么复杂,它的上界是否可以是多项式?

定义 2.12 我们称是多项式有界的,若

例 2.9 函数是多项式有界的吗?函数呢?

解:对于,根据Stiring公式有: 所以不是多项式有界的。

也可以用反证法,假设是多项式有界的,那么存在一个常数和正整数,当时,总有。若取,即。这显然是矛盾的,因为不是指数有界的。

对于,同样根据Stiring公式有: 所以,是多项式有界的。

3. 求解复杂度

根据算法策略的不同,算法可以分为迭代和递归两种。这两种策略有着不同的递推关系,下面我们讨论如何根据算法的递推关系求解算法的复杂度。

3.1. 减而治之

减而治之的问题的递推关系通常如下所示 其中为一常数,通常是幂函数。

这类的递推关系又称为线性递推关系,若,我们称上式为常系数齐次线性递推关系,否则称为常系数非齐次线性递推关系。关于线性递推关系的求解,在各种组合数学的参考书中都能找到,这里我们不再赘述。我们主要关注一下如何求解分而治之问题的递推关系。

3.2. 分而治之

分而治之的问题通常具有以下形式的递推式。 对于这种类型的递推式,最基本的方法就是不断将它进行替换,直至等式右边出现 注意到,实际上当递归到递归基时,有,即。所以上式变为 等式右边的第一项是一个幂函数,第二项是一个几何级数。我们只需考虑这两项的增长性孰大孰小,即可表示出。于是,产生了主定理

定理 3.1(主定理)

如上述定义。

  1. 若存在一个任意小的常数,使得。则
  2. 若存在常数,使得,则
  3. 若存在一个任意小的常数,使得,则

请注意,主定理只是给出渐进意义上的增长性,而并没有给出的真正解。

要理解主定理并不难,首先回忆我们上述得出的的表达式。 不难发现,决定的增长性无外乎以下三种情况:

  • 比较小,那么第一项占主导地位,于是
  • 当上述和式中的每一项都相互成比例时,那么就是与一个对数因子的乘积。
  • 大于时,第二项是以为首项的递减的几何级数,所以成正比。

下面我们简单证明一下第二种情况。设,我们说明当很大时,和式中的任意两项是成比例的。 接下来来看一些主定理的应用。

例 3.1 设递推关系,试求的增长性。

解:,所以此时属于第一种情况。

例 3.2 设递推关系,试求的增长性。

解:,属于第二种情况。所以

例 3.3 设递推关系,试求的增长性。

解:,属于第三种情况。

例 3.4 设递推关系,试求的增长性。

解:,属于第三种情况。

例 3.5 设递推关系,试求的增长性。

解:这种情况比较棘手,不适用与主定理的情况。我们考虑进行变量替换。设,则原式变为 再稍加变换,令,得 ,属于第二种情况,根据主定理有

所以,将回代,得:

3.3. 分摊分析

附录

附录中给出了一些有用的数学公式。

1. 对数函数

对于对数函数,通常有下面这些省略性的记号。

  • (在计算机科学中,我们默认底数为,在数学上则是默认为
  • (自然对数)
  • (取幂)
  • (复合运算)

除了以上的记号外,还有一个重要的记号是,表示多重对数函数。它来源于多重函数

定义 记号表示函数重映射。

请不要将它与数学上的高阶导数和函数的次幂弄混。

由于只有在才有定义,而,所以我们只能连续取对数直到函数值小于等于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.