算法分析

Introduction

有各种原因要求我们分析算法,像预测算法性能,比较不同算法优劣等,其中很实际的一条原因是为了避免性能错误,要对自己算法的性能有个概念。

科学方法(scientific method)也适用于算法分析,它提供了一个预测性能和比较算法的框架:

  • Observe 观察真实世界的某些特征
  • Hypothesize 根据观察结果提出假设模型
  • Predict 用模型预测未来的事件
  • Verify 继续观察来核实预测的准确性
  • Validate 重复直到预测和观察一致

此外还有两条原则:

  • reproduceible 实验得是别人可以重现的
  • falsifiable 假设可以被实验证伪

Observations

先给出一个例子,叫 3-sum 问题,即给定一组数,问三数之和为零的有哪些组合,据说在计算几何中很有用。暴力求解的算法很简单,三个 for 循环。

3-Sum Brute-Force Algorithm

public class ThreeSum {
public static int count(int[] a) {
int N = a.length;
int count = 0;
for (int i = 0; i < N; i++)
for (int j = i + 1; j < N; j++)
for (int k = j + 1; k < N; k++)
if (a[i] + a[j] + a[k] == 0)
count++;
return count;
} public static void main(String[] args) {
int[] a = In.readInts(args[0]);
StdOut.println(count(a));
}
}

分析算法时的观测即是看程序的运行时间,你可以用秒表计时,或是使用 java 中计时的类。

public static void main(String[] args) {
int[] a = In.readInts(args[0]);
Stopwatch stopwatch = new Stopwatch();
StdOut.println(ThreeSum.conut(a));
double time = stopwatch.elapsedTime();
}

运行这个程序,就可以知道上述暴力算法的运行时间。

有了这些数据,我们可以画出运行时间和输入数据规模 N 的函数图像。

这个不好求二者关系,我们还可以画成双对数的形式。

这张图看起来好求些,但其实我们都不用画图。

每次运行将数据规模翻倍,由式子 \(aN^{b}\) ,前后运行时间之比即为 \(2^{b}\) ,再取 \(lg\) ,就可大致知道 b 的级别。知道 b 再随便带入一点计算,即可得到 a 。求出 a 和 b 就可以用输入数据规模 N 来预测运行时间了。

b 由算法和输入数据决定,a 则还会受到软硬件等各方面的影响,所以我们很难得到准确的测量。但比起其他科学,我们的实验成本很低,可以进行大量的实验。

Mathematical Models

原则上我们仍然可能构造出一个数学模型来描述任意程序的运行时间,总运行时间即为每个操作的耗时和频率乘积的总和。耗时和计算机硬件,Java 编译器,操作系统等有关,后者则取决于算法和输入。

但我们不是理论家,不需要这么精确,可以简化计算,粗略的估计依然很有用。

Simplification 1: Cost Model

成本模型,用开销最大的或执行次数最多的基本操作来估计运行时间,这里选取访问数组这个操作。

Simplification 2: Tilde Notation

忽略式子中的低阶项,当 N 很大的时候,低阶项并没有什么影响,N 很小时的情况我们并不关心。

f(N)~g(N) 代表着 \(\lim\limits_{x \rightarrow 0} \frac{f(N)}{g(N)}\) (附:LaTeX)。

Order-of-Growth Classifications

增长数量级的分类,我们在实现算法时使用了几种结构性的原语,像循环,嵌套语句,所以成本增长的数量级一般都是问题规模 N 的若干函数之一。

从双对数图更能说明平方级别和立方级别的算法对于大规模的问题是不可用的。

因此,我们自然希望为各种基础问题找到对数级别、线性级别或是线性对数级别的算法。

上面提到的 3-Sum 问题,暴力求解是立方级别,我们可以用二分查找对其做些改进。

Binary Search

public static int binarySearch(int[] a, int key) {
int lo = 0, hi = a.length-1;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
if (key < a[mid]) hi = mid - 1;
else if (key > a[mid]) lo = mid + 1;
else return mid;
}
return -1;
}

二分查找在大小为 N 的有序数组中最多只要进行 \(1 + lgN\) 次比较,也就是对数级别。

可以递推地证明这一结论。

于是我们第一步要对数组进行排序,随便来个平方级别的插入排序,然后对数组中每一对 a[i] 和 a[j] ,二分查找 -(a[i] + a[j]) ,这是 \(N^{2}logN\) 级别,总的来说这算法是 \(N^{2}logN\) 级别的。

可以看出这样也比原来立方的级别好很多,更好的数量级,更好的性能表现。

Theory of Algorithms

而实际情况会比上面的例子复杂些,事实是对不同的输入,算法会有不同的性能表现。像二分查找,其实最好的情况是常数级别的。所以针对不同的输入,我们经常需要从不同的角度来分析问题,可分为最好、最差和平均三种类型。但实际的数据不会简单的就是这三种情况,更一般地做法是我们按最坏情况设计算法或是考虑随机情况下某种概率的性能保证。

在算法理论中,我们想要知道问题有多困难,解决它的最佳算法是什么。在分析中,我们希望去掉尽可能多的细节,将分析做到只差一个常数倍数的精度,增长阶数分析可以做到。我们还想做到不用考虑输入情况,所以将重点放在针对最坏情况的算法设计,然后就可以只用增长阶数来讨论算法性能。

为了描述性能的界限,有一些常用的记号。

举例来说,1-Sum 问题要找到数组中零元素的个数,我们想要知道问题的难度和最佳算法。暴力求解需要检查数组的每个元素,运行时间是 \(O(N)\),这为最佳算法提供了一个上界。而其实为了确定数组里有几个零也必须检查数组的每个空,所以解决这个问题的最佳算法的下界也很好确定为 \(\Omega(N)\)。除去常数,上下界吻合,暴力算法就是 1-Sum 的最佳算法,运行时间为 \(\Theta(N)\)。对于这个简单的问题,找到最佳算法很简单。

对于更复杂的问题,确定上下界就更困难,更别说找到吻合的上下界了。再举例来看3-Sum问题,暴力算法的运行时间是 \(O(N^{3})\),用二分查找改进的算法则是 \(O(N^{2}logN)\),这是一个更好的上界。下界按上一个例子的想法可以为 \(\Omega(N)\)。上下界之间存在间隔,我们就不知道求解 3-Sum 问题的难度,也不知道最优算法是什么,这是个开放问题。我们寻找新的能够降低上界的算法或者寻找提高下界的方法,以此来了解问题的计算难度,找到最佳算法,这是一个富有魅力的研究领域,很多问题上下界之间仍有间隔。

但关注最坏情况对我们来说可能有些过于悲观,还是留给算法理论学家们吧。而且真的要预测性能和比较算法时,需要比常数因子级误差更准确的分析。所以我们讲了波浪(~)记号,以及在算法理论中使用的常用记号。需要注意的是,很多人错把 \(O\) 分析的结果当作运行时间的近似模型,其实应该是问题更好的上界,这门课中,我们使用波浪记号来表示近似模型。

Memory

内存的使用和具体的机器实现相关,我们用 典型 这个词暗示和机器相关的值。

典型的 Java 实现用 1 字节表示一个 boolean 值,因为计算机访问内存的方式都是一次 1 字节。同时,我们假设表示机器地址需要 8 字节,这是现在广泛使用的 64 位架构中典型表示方式,许多老式的 32 位架构只使用 4 字节表示机器地址。

要知道一个对象所使用的内存量,需要将所有实例变量使用的内存与对象本身的开销(一般是 16 字节)相加。这些开销包括一个指向对象的类的引用、垃圾收集信息以及同步信息。另外,一般内存的使用都会被填充为 8 字节( 64 位计算机中的机器字)的倍数。

Java 中数组被实现为对象,它们一般都会因为记录长度而需要额外的内存。一个原始数据类型的数组一般需要 24 字节的头信息( 16 字节的对象开销, 4 字节用于保存长度以及 4 填充字节)再加上保存值所需的内存。

String 的标准实现含有 4 个实例变量:一个指向字符数组的引用( 8 字节)和三个 int 值(各 4 字节)。第一个 int 值描述的是字符数组中的偏移量,第二个 int 值是一个计数器(字符串的长度),这个 String 对象所表示的字符串由 value[offset] 到 value[offset + count - 1] 中的字符组成。String 对象中的第三个 int 值是一个散列值,表示这个对象的哈希码。

字符串处理经常会和子字符串打交道,所以 Java 对字符串的表示希望能够避免复制字符串中的字符。当你调用 substring() 方法时,就创建了一个新的 String 对象( 40 字节),但它仍然重用了相同的 value[] 数组,因此该字符串只会使用 40 字节的内存。含有原始字符串的字符数组的别名存在于子字符串中,子字符串对象的偏移量和长度域标记了子字符串的位置。换句话说,一个子字符串所需的额外内存是一个常数,构造一个子字符串所需时间也是常数,即使字符串和子字符串的长度极大也是这样。

这些基础机制能够有效帮助我们估计大量程序对内存的使用情况,但许多复杂的因素仍然会是这个任务变得更加困难。当涉及函数调用时,内存的消耗就变成了一个复杂的动态过程,这种动态过程使准确估计一个程序内存使用变得极为困难。

Analysis of Algorithms的更多相关文章

  1. 算法分析 Analysis of Algorithms -------GeekforGeeker 翻译

    算法分析 Analysis of Algorithms 为什么要做性能分析?Why performance analysis? 在计算机领域有很多重要的因素我们要考虑 比如用户友好度,模块化, 安全性 ...

  2. 6.046 Design and Analysis of Algorithms

    课程信息 6.046 Design and Analysis of Algorithms

  3. "Mathematical Analysis of Algorithms" 阅读心得

    "Mathematical Analysis of Algorithms" 阅读心得 "Mathematical Analysis of Algorithms" ...

  4. 《Mathematical Analysis of Algorithms》中有关“选择第t大的数”的算法分析

    开头废话 这个问题是Donald.E.Knuth在他发表的论文Mathematical Analysis of Algorithms中提到的,这里对他的算法分析过程给出了更详细的解释. 问题描述: 给 ...

  5. 612.1.002 ALGS4 | Analysis of Algorithms

    我们生活在大数的时代 培养数量级的敏感! Tip:见招拆招 作为工程师,你先要能实现出来. 充实基础,没有什么不好意思 哪怕不完美.但是有时候完成比完美更重要. 之后再去想优化 P.S.作者Rober ...

  6. Analysis of algorithms: observation

    例子: 3-Sum 给定N个整数,这里面有多少个三元组,使其三个整数相加为0,如上面的例子为有4个三元组. 这个问题是许多问题如计算机几何,图形学等的基础. 用简单粗暴的方式来解决3-Sum问题 通过 ...

  7. Time complexity analysis of algorithms

    时间复杂性的计算一般而言,较小的问题所需要的运行时间通常要比较大的问题所需要的时间少.设一个程序P所占用的时间为T,则 T(P)=编译时间+运行时间. 编译时间与实例特征是无关的,且可假设一个编译过的 ...

  8. AlgorithmsI Exercises: Analysis of Algorithms

    Question 1 Suppose that you time a program as a function of N and producethe following table. N seco ...

  9. Stability Analysis of Algorithms

    算法(Algorithm)是指用来操作数据.解决程序问题的一组方法.对于同一个问题,使用不同的算法,也许最终得到的结果是一样的,比如排序就有前面的十大经典排序和几种奇葩排序,虽然结果相同,但在过程中消 ...

随机推荐

  1. FocusBI: SSIS 开发案例(原创)

    关注微信公众号:FocusBI 查看更多文章:加QQ群:808774277 获取学习资料和一起探讨问题. <商业智能教程>pdf下载地址 链接:https://pan.baidu.com/ ...

  2. HandlerInterceptor与MethodInterceptor

    HandlerInterceptor是springMVC项目中的拦截器,它拦截的目标是请求的地址,比MethodInterceptor先执行.实现一个HandlerInterceptor拦截器可以直接 ...

  3. jquery里prop和attr的区别

    本文通过具体的实例来讲述jquery里prop和attr的区别及使用方法. 在jquery里,我们要获取一个标签元素的属性,可以用attr或者prop,那么两者有什么区别呢? 其实很简单: attr可 ...

  4. C# 空合并运算符 ??

    C#语言中,??运算符称为空合并运算符: a??b形式的空合并表达式要求a为可以为null的类型或引用类型.如果a为非null,则a??b的结果为a:否则,结果为b.仅当a为null时,该操作才计算b ...

  5. ASP.NET中让图片以二进制的形式存储在数据库中

    今早有个网友问到我这问题,以前我都是直接在数据库中存文件名的,还没有试过存储整张图片到数据库中,上网搜索了一下,自己又测试了一番,代码如下:建立保存图片的表的SQL语句: USE [niunantes ...

  6. 十六、Condition等待通知

    一.简介 我们可以使用syncronized和wait,notify实现等待通知.而syncronized的高级实现Lock,也可以实现等待通知,需要构造Condition的实例对象. JDK文档:h ...

  7. BZOJ P1212 [HNOI2004] L语言

    标点符号的出现晚于文字的出现,所以以前的语言都是没有标点的.现在你要处理的就是一段没有标点的文章. 一段文章T是由若干小写字母构成.一个单词W也是由若干小写字母构成.一个字典D是若干个单词的集合. 我 ...

  8. ES6框架的搭建

    1.引入traceur.js  http://google.github.io/traceur-compiler/bin/traceur.js 2.将Traceur编译器用于网页 new traceu ...

  9. webpack+vue2实现旅游网小demo

    这两天自己练习做了一个webpack+vue2的旅游app小项目,涉及到的内容是vue组件.vue路由以及webpack打包.         目录文件设计: 有兴趣的可到我的百度网盘下载 链接: h ...

  10. JavaScript - 收藏集 - 掘金

    Angular 中的响应式编程 -- 浅淡 Rx 的流式思维 - 掘金第一节:初识Angular-CLI第二节:登录组件的构建第三节:建立一个待办事项应用第四节:进化!模块化你的应用第五节:多用户版本 ...