算法及算法的评价
算法
算法是对特定问题求解步骤的一种描述,它是指令的有点序列,其中每一条指令表示一个或多个操作。
算法的五个重要特性
有穷性、确定性、可行性、具有输入、具有输出。
好算法的特征
通常一个好的算法应该达到以下目标:
1.正确性:算法应当能正确地解决求解问题。
2.可读性:算法应当具有良好的可读性,以助人们理解。
3.健壮性:当输入非法的数据时,算法也能适当的做出反应或进行处理,而不会产生莫名奇妙的输出结果。
4.效率与低存储量需求:效率是指算法的执行时间、存储量需求是指算法执行过程中所需要的最大存储空间,这两者都与问题规模有关。
渐进分析:复杂度和大O记号的引入
回归最初的问题,随着问题的增长,计算成本应该如何增长?这里的我们只关心足够大的问题规模,注重考察成本的增长趋势。
渐进分析:在问题规模足够大后,计算成本增长的分析。
时间复杂度和空间复杂度
是算法的效率的度量。
时间复杂度:一个语句的频度是指该语句在算法中被重复执行的次数。算法中的所有语句的频度之和记作\(T(n)\),它是该算法问题规模n的函数,时间复杂度主要分析\(T(n)\)的数量级,又因为算法中基本运算(最深层循环内的语句)的频度与\(T(n)\)同数量级,所以通常采用算法中基本运算的频度\(f(n)\)来分析算法的时间复杂度。因此,算法的时间复杂度记为:\(T(n) = O(f(n))\)
大O记号(big-O notation)
\(T(n) = O(f(n)) \quad\) \(iff \quad\) \(∃ \ c > 0\),当 \(n >> 2\)后,有\(T(n) < c*f(n)\)
\(\sqrt{5n*[3n*(n+2) + 4] + 6} < \sqrt{5n * [6n^2 + 4] + 6} < \sqrt{35n^3 + 6} < 6 * n^{1.5} = O(n^{1.5})\)
与\(T(n)\)相比,\(f(n)\)更为简洁,但依然反映着前者的增长趋势。
常系数可忽略:\(O(f(n)) \ = \ O(c*f(n))\)
低次项可忽略:$O(n^a + n^b) = O(n^a) \(, 当\)a > b > 0$
时间复杂度:\(O(1)\)
常数(constant function)
\(2 = 2013 = 2013 * 2013 = O(1)\),甚至\(2013^{2013} = O(1)\) //含RAM各基本操作
这类算法效率最高(总不能奢望不劳而获吧)
什么样的代码段对应于常数执行时间? (应具体分析)
一定不含循环?
for(i = 0; i < n; i += n / 2013 + 1);
for(i = 1; i < n; i = 1 << i) // log*n,几乎为常数
一定不含分支转向?
if((n + m) * (n + m) < 4 * n * m) goto UNREACHABLE; //不考虑溢出
一定不能有(递归调用)?
if(2 == (n * n) % 5) O1(n);
时间复杂度:\(O(log^cn)\)
**对数\(log(n)\) **
\(lnn\)| $ lgn $| $log_{100}n $| \(log_{2013} n\)
**常底数无所谓 **
\({\forall}\ a,\ b > 0, \ log_an = \ log_ab * \ log_bn = \ Θ(log_bn)\)
**常数次幂无所谓 **
\({\forall}\ c \ > \ 0 ,\ logn^c \ = \ c * logn = Θ(logn)\)
**对数多项式(ploy-log function) **
\(123*log^{321}n \ + \ log^105(n^2 - n + 1) \ =\ Θ(log^{321}n)\)
这类算法非常有效,复杂度无限接近于常数:\({\forall} \ c \ > \ 0, \ logn \ = \ O(n^c)\)
时间复杂度:\(O(n^c)\)
**多项式(ploynomial function) : **
\(100n + 200 = O(n)\)
\((100n - 500)(20n^2 - 300n = 2013) \ = \ O(n * n^2) \ = \ O(n^3)\)
\((2013n^2 -20) / \ (1999n - 1) \ = \ O(n^2/n) \ = O(n)\)
一般地:\(a_kn^k + a_{k-1}n^{k-1} + ... + a_1n + a_0 \ = \ O(n^k), \ a_k > 0\)
线性(linear function):所有\(\ O(n)\)类函数
从\(O(n)\)到\(O(n^2)\):编程算法题的主要覆盖范围
幂:\([(n^{2013} -24n^{2009})^{1/3} + 512n^{567} - 1978n^{123}]^{1/11} \ = \ O(n^{61})\)
时间复杂度:\(O(2^n)\)
**指数(exponential function): **\(T(n) \ = \ a^n\)
\({\forall} \ c \ > \ 1,\ n^c \ = \ O(2^n)\)
\(n^{1000} \ = \ O(1.0000001^n) \ = \ O(2^n)\)
\(1.0000001^n = Ω(n^{1000})\)
这类算法的计算成本增长极快,通常被认为不可忍受。
从\(O(n^c)\)到\(O(2^n)\)是从有效算法到无效算法的分水岭,很多问题\(O(2^n)\)的算法往往显而易见,然后设计出\(O(n^c)\)的算法却极其的不易,甚至,有时候注定是徒劳无功的,我们把这些问题分为NP和非NP问题。
复杂度层次
最坏时间复杂度:指在最坏的情况下,算法的时间复杂度。
平均时间复杂度:指所有输入等可能的情况下,算法的期望运行时间。
最好时间复杂度:值在最好的情况下,算法的时间复杂度。
均摊时间复杂度:在代码执行的所有复杂度情况中绝大部分是低级别的复杂度,个别情况是高级别复杂度且发生具有时序关系时,可以将个别高级别复杂度均摊到低级别复杂度上。基本上均摊结果就等于低级别复杂度。
举例:有一个长度为n的数组,如果数组没满,就往里插入一个数,如果数组满了,就遍历求和.那么绝大多数情况下都是\(O(1)\),只有最后一次是\(O(n)\),均摊以后就是\(O(1)\)
复杂度振荡:数组等数据结构的扩容和缩容之间出现,原因是没有处理好扩容和缩容因子之间的关系。
平均分析 VS 分摊分析
平均复杂度或期望复杂度(average/expected complexity)
根据数据结构各种操作出现概率的分布,将对应的成本加权平均。
各种可能的操作,作为独立事件分别考察,割裂了操作之间的相关性 和连贯性,往往不能准确地评判数据结构和算法的真实性能。
分摊复杂度(amortized complexity)
对数据结构连续地实施足够多次的操作,所需总体成本分摊至单次操作。
从实际可行的角度,对一系列操作做整体的考量,更加忠实地刻画了可能出现的操作序列,可以更为精准地评判数据结构和算法的真实性能。
空间复杂度
算法的空间复杂度\(S(n)\),定义为该算法所耗费的辅助存储空间,它是问题规模n的函数。渐进空间复杂度通常简称为空间复杂度,记作\(S(n) = O(g(n))\)。
算法的 原地工作是指算法所需的辅助存储空间是常量,即O(1)。