Analysis of Algorithms 算法性能分析方法
1.Observation观察
使用编程语言实现一个算法,测试其运行时间是一件非常重要的事情。
常用的有以下3中方法(针对Java程序)
方法1:手动测量
直接使用计时器来测量运行时间(估计没有人会接受这种方法)
方法2:使用程序自动测量
在java中Stopwatch类里面有elapsedTime()方法可以测量程序的运行时间。
Stopwatch stopwatch=new Stopwatch();
double time =stopwatch.elapsedTime();
好处是使用方便,效果明显;缺点是当运行时间较长的,需要观察者等待。
方法3:数据模型分析
在下表的数据中:
运行时间TN与N的关系的非线性的,不易确定其模型,如下图:
那么可以两边取对数,取得其重对数图像,如下图:
由于呈线性关系,假设其模型为lg(TN)=blg(N)+c
对以上模型两遍取平方,得到幂定律:TN=aN^b
因此,通过数据所推演的模型是符合幂定律的。接着要推算常数a与b的值
对于常数b,使用双倍假设法r=T2N/TN=2^b,所以b=lg®
对于常数a,使用过表中的数据即可求出近似值
2.mathematical model数学模型
一般而言,都是使用开销最大、频率最高的操作来代表算法的的运行时间的。例如具体开销的1/6N^3+100N+16,那么描述的时候就近似于~1/6N3
常用的几个运行时间的模型如下图所示:
3.order-of-growth classifications 增长阶数分类
增长阶数分类主要有以下
开销 | 英文名称 | 代表算法 |
---|---|---|
1 | constant | 一行代码 |
lgN | logarithmic | 对有序数列的二叉搜索 |
N | linear | 循环 |
NlgN | linearithmic | 归并排序 |
N^2 | quardratic | 二重循环 |
N^3 | cubic | 三重循环 |
2^N | exponential | 查子集 |
各类别的增长曲线如下图:
这里主要分析一下3-sum问题:即从一个集合中找到3个数,是这3个数的和是指定的值(如a1+a2+a3=10)。
通常,我们可以使用三重循环
来完成
/**
* 三重循环O(N3)
*/
public void solution1() {
List<Integer> numList = new ArrayList<>();
for (int i = 0; i < numList.size(); i++) {
// 内循环避开已遍历过的树
for (int j = i + 1; j < numList.size(); j++) {
for (int k = j + 1; j < numList.size(); k++) {
// 找出和未10的数
if (numList.get(i) + numList.get(j) + numList.get(k) == 10) {
// 输出操作(没写)
}
}
}
}
}
分析其时间复杂度,发现内循环的一条语句需要重复执行C(3,N)次
其中,C(3,N)=[n(n-1)(n-2)]/3!~n^3/6
这是3阶的情况,时间代价不可接受。
换一种思路,先使用选择排序法
将数列从小到大排序,消耗NlgN的时间
再使用二重循环
计算a[i]+a[j]的和s,再使用二叉搜索
找10-s的值是否在数列里,消耗N*NlgN的时间(进行N的平方次搜索)
/**
* 改进方法O(N2lgN)
*/
public void solution2() {
List<Integer> numList = new ArrayList<>();
// 选择排序法排序,sort是选择排序函数
sort(numList);
for (int i = 0; i < numList.size(); i++) {
for (int j = i + 1; j < numList.size(); j++) {
// bSearch是二叉搜索函数(没写)
if (bSearch(-(numList.get(i) + numList.get(j)))) {
// 输出操作(没写)
}
}
}
}
4.算法分析理论
对于解决某种类型的算法存在最好的情况和最坏的情况,其中,最好的情况称为下界,而最坏的情况称为上界:
最坏的情况(上界)
|----------------|
存在优化的算法
|----------------|
最好的情况(下界)
在算法分析理论中由以下常用的标识
标识 | 示例 | 说明 |
---|---|---|
~ | ~N | 运行时间近似于N*C (C是常数) |
big Oh | O(N) | 运行时间的上界是N*C |
big Theta | linear | 运行时间就是N*C |
big Omega | linearithmic | 运行时间的下界是N*C |
通常,我们都是错误地将O(N)使用成~N,但随着使用人数增加,尽管是错误的,大家都是工人这两种标识都是一样的。
5.内存消耗(空间复杂度)
下面是Java基本类型的占用的内存字节数
type | bytes |
---|---|
boolean | 1 |
byte | 1 |
char | 2 |
int | 4 |
float | 4 |
long | 8 |
double | 8 |
char[] | 2N+24(其中24的固定字节) |
int[] | 4N+24 |
double[] | 8N+24 |
char[][] | ~2MN |
int[][] | ~4MN |
double[][] | ~8MN |
对于对象而言,其所占字节有以下部分组成,结合下图参考下表
part | bytes |
---|---|
Object overhead | 16 |
Reference | 8 |
padding | 将占用内存凑成8Byte的整数倍 |