『算法』读书笔记 1.4算法分析 Part2
Chapter 1
本章结构
1.1Java语法
1.2数据抽象
1.3集合类抽象数据类型:背包 (Bags) 、队列 (Queues) 、栈 (Stacks)
1.4算法分析
1.5连通性问题-Case Study: Union - Find ADT
首先来看一个对增长数量级的常见假设的总结:
1.常数级别
增长数量级为1
典型代码:
a = b + c
普通语句。大多数的Java操作所需要的时间都是常数级别的。
2.对数级别
增长数量级为log N
典型例子:二分查找
因为对数的底数和增长的数量级无关(不同的底数仅相当于一个常数因子),所以我们在说明对数级别时使用log N即可。
3.线性级别
增长数量级为N
典型例子:单层循环(找出最大元素)
double max = a[0]; for (int i = 1; i < N; i++) if (a[i] > max) max = a[i];
4.线性对数级别
增长数量级为N log N
典型例子:归并排序
以下三种简单的解法通常被称之为暴力算法Brute-force solution,非常耗时间。
5.平方级别
增长数量级为N^2
典型例子:双层循环(检查所有元素对)
for (int i = 0; i < N; i++) for (int j = i+1; j < N; j++) if (a[i] + a[j] ==0) cnt++;
6.立方级别
增长数量级为N^3
典型例子:三层循环(检查所有三元组)
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) cnt++;
7.指数级别
增长数量级为2^n
典型例子:穷举查找
指数级别的算法非常慢。
必须指出,当输入规模非常大时,平方级别、立方级别和指数级别的算法都是不可取的。线性对数级别的算法在实践中非常重要。
那么,如何对上面的2-sum和3-sum算法进行改进以实现更高的效率?
以2-sum为例进行说明,count的实现利用了排序和二分查找。我们知道,二分查找的增长级别为log N,又count含有一单层循环,所以整个2-sum运行时间的增长数量级可以从之前的N^2减少到Nlog N。
import java.util.Arrays; public class TwoSumFast { public static int count(int[] a) { Arrays.sort(a); int N = a.length; int cnt = 0; for (int i = 0; i < N; i++) if (BinarySearch.rank(-a[i], a) > i) cnt++; return cnt; } public static void main(String[] args) { int[] a = In.readInts(args[0]); StdOut.println(count(a)); } }
----------------------------------------------------------------------------------------------------------------------------------------------
利用倍率实验Doubling ratio experiments可以有效地预测任意程序的性能并判断它们的运行时间大致的增长数量级。
public class DoublingRatio { public static double timeTrial(int N) { int MAX = 1000000; int[] a = new int[N]; for (int i = 0; i < N; i++) a[i] = StdRandom.uniform(-MAX, MAX); StopWatch time = new StopWatch(); int cnt = ThreeSum.count(a); return time.elapseTime(); } public static void main(String[] args) { double prev = timeTrial(125); for (int N = 250; true; N += N) { double time = timeTrial(N); StdOut.printf("%6d %7.1f",N ,time); StdOut.printf("5.1f", time/prev); prev = time; } } }
根据倍率实验的结果,我们可以评估某算法解决大型问题的可行性,以及评估当更快的计算机被使用时所产生的价值。对于后者,因为更新的计算机往往被用于处理更大规模更为复杂的输入模型,此处意在检验,如果计算机的处理能力和数据规模进行同级别的扩大,运算时间会发生怎样的变化。
-----------------------------------------------------------------------------------------------------------------------------------------------
进行性能分析时我们还需要注意:
1.低级项中的常数系数,他们可能很大以至于不能被忽略;
2.非决定性的内循环的影响;
3.系统因素,例如垃圾收集器或者是JIT编译器可能对实验结果进行干扰;
4.对输入的强烈依赖,随机输入模型可能是不切实际的,有些时候需要从极度悲观的角度为性能提供保证。
-----------------------------------------内存部分-----------------------------------------------------
这一块主要介绍了各种不同的数据类型对内存的占用情况,总结如下:
1.对象
对象开销16 bytes + 封装对象中原始数据类型的开销 + 填充字节;
2.链表
Node对象40 bytes (对象开销16bytes + 额外开销8bytes + item和next引用8bytes*2) + 引用对象所占用的内存;
3.数组(Java中数组被实现为对象)
类型 |
字节数 |
|
int[] | 24+4N | ~4N |
double[] | 24+8N | ~8N |
Date[] | 24+40N | ~40N |
double[][] | 24+32M+8MN | ~8MN |
4.字符串对象
String的标准实现含有4个实例变量:一个指向字符数组的引用(8字节,即字符串的值)和三个int值(各4字节,包括偏移量,字符串的长度和散列值)。
字符串对象这样设计的原因是,当你调用substring()方法时,虽然创建了一个新的String对象,但它仍然重用了相同的value[]数组。一个子字符串所需要的额外内存是一个常数(40字节),构建一个子字符串所需要的时间也是常数。这是许多基础字符串处理算法的效率的关键所在。
----------------------------
By way of conclusion,较快的算法一般都比暴力算法更为复杂。但是为了更高的效率,很多时候还是值得花点功夫去研究的。
Clear, efficient and elegant code is our destination.