算法(第四版)1.4摘抄
我们会使用数学分析为算法成本建立简洁模型并使用实验数据验证这些模型。
(一)
第一个挑战是:决定如何定量测量程序的运行时间?
......
一般来说,数学模型中的对数项是不能忽略的,但在倍率假设中它在预测性能的公式中的作用并不那么重要。
对于编写的每个程序,你都需要能够回答这个基本问题:“该程序能在可接受的时间内处理这些数据吗?”
我们会按照以下方式解决问题:
1. 实现并分析该问题的一种简单的解法。我们通常将它们称为暴力算法。如ThreeSum
2. 考查算法的各种改进,它们通常都能降低算法所需的运行时间的增长数量级。如ThreesumFast。
3. 用实验证明新的算法更快。
(二)
在对程序的性能进行仔细分析的时候,可能得到不一样的结果。它们是由于我们的猜想基于的一个多多个假设并不完全正确所造成的。
我们需要注意的事项有:
1. 大常数
在首项近似中,我们一般会忽略低级项中的常数系数,但这可能是错的。例如2N2+cN。因此,我们要对可能的大常数保存敏感。
2. 非决定性的内循环
内循环是决定性因素的假设并不总是正确的。错误的成本模型可能无法得到真正的内循环。
3. 指令时间
每条指令执行所需的时间总是相同的假设是不正确的。
例如说:大多数现代计算机系统都会使用缓存技术来组织内存,在这种情况下访问大数组中的若干个并不相邻的元素需要的时间可能很长。
4. 系统因素
例如有好几个程序正在运行。
5. 不分伯仲
常常出现的一种情况是:其中在某些场景中更快,而在另一些场景中更慢。有些程序员特别喜欢投入大量的精力进行比赛并找出“最佳”实现,但此类工作最好还是留给专家。
6. 对输入的强烈依赖
以ThreeSum为例,如果我们的问题是“输入中是否存在和为0的三个整数”,修改threeSum病返回bool值,那么如果输入中的头三个整数的和为0,该程序的运行时间的增长数量级为常数级别;如果不含有这样的三个整数,程序的运行时间的增长数量级为立方级别。
一个重要的问题是对于输入的依赖。如何处理这一问题?
方法一:对最坏情况下的性能的保证
子计算机系统中最坏情况是非常现实的忧虑,因为程序的输入可能来自另外一个用户而非自然界。
方法二:随机化算法
为性能提供保证的一种重要方法是引入随机性。例如,快速排序在最坏情况下的性能是平方级的,但是通过随机打乱输入,根据概率我们可以保证它的性能是线性对数的。散列算法在最坏情况下的性能是线性级别的,但是根据概率我们可以保证它的运行时间是常数级别的。
方法三:均摊分析
这里针对的问题是:操作序列可能导致性能不同。
我们的解决办法是:记录所有操作的总成本并除以操作总数来将成本均摊。
(三 总结)
速度极慢的程序和不正确的程序一样无用。
在编程领域中,最常见的错误或许就是过于关注程序的性能。首要任务是应该是写出清晰正确的代码。仅仅为了提高运行速度而修改程序的事最好留给专家们来做。事实上,这么做常常会降低生产效率,因为它会产生复杂而难以理解的代码。所谓“在编程领域,不成熟的优化是所有的罪恶之源”。
第二个常见错误或许是完全忽略程序的性能。
几个重要的图表
(1.5部分)
为了说明我们设计和分析算法的基本方法,我们现在来学习一个具体的例子,我们的目的是强调以下几点:
优秀的算法因为能够解决实际问题而编的更为重要;
高效算法的代码也可以很简单
理解某个实现的性能特点是一项有趣而令人满足的挑战;
在解决同一问题的多种算法之间进行选择时,科学方法是一种重要的工具;
迭代式改进能够让算法的效率越来越高。
我们再次强调:
完整而详细地定义问题,找出解决问题所必须的基本抽象操作并定义一份API;
简洁地实现一种初级算法,给出一个精心组织的开发用例并使用实际数据作为输入;
当实现所能解决的问题的最大规模达不到期望时决定改机还是放弃;
逐步改进实现,通过经验性分析或(和)数学分析验证改进后的效果;
用更高层次的抽象表示数据结构或算法来设计更高级的改进版本;
如果可能尽量为最坏情况下的性能提供保证,但在处理普通数据时也要有良好的性能;
在适当的时候将更细致的深入研究留给有经验的研究者并继续解决下一个问题。