算法分析
1.例子(3-sum)
在3-sum之前,先来看一种表示计时器的抽象数据类型:
代码如下:
public class Stopwatch {
private final long start;
public Stopwatch() {
start = System.currentTimeMillis();
}
public double elapsedTime() {
long now = System.currentTimeMillis();
return (now - start) / 1000.0;
}
public static void main(String[] args) {
int n = Integer.parseInt(args[0]);
// sum of square roots of integers from 1 to n using Math.sqrt(x).
Stopwatch timer1 = new Stopwatch();
double sum1 = 0.0;
for (int i = 1; i <= n; i++) {
sum1 += Math.sqrt(i);
}
double time1 = timer1.elapsedTime();
StdOut.printf("%e (%.2f seconds)\n", sum1, time1);
// sum of square roots of integers from 1 to n using Math.pow(x, 0.5).
Stopwatch timer2 = new Stopwatch();
double sum2 = 0.0;
for (int i = 1; i <= n; i++) {
sum2 += Math.pow(i, 0.5);
}
double time2 = timer2.elapsedTime();
StdOut.printf("%e (%.2f seconds)\n", sum2, time2);
}
}
下面看3-sum程序描述:统计一个文件中所有和为0的三元整数元组的数量(假设整数不会溢出)。作为测试输入,使用1Mints.txt文件,它含有100万个随机生成的int值。1Mints.txt的第二个、第八个和第十个元组的和均为0.后面还有1Kints.txt、2Kints.txt、4Kints.txt、8Kints.txt文件,他们分别含有1Mints.txt中的1000、2000、4000、8000个整数。所以这样的整数元组在1Kints.txt中共有70组,在2Kints.txt中有528组,在4Kints.txt中有4039组。那么越往后,程序需要多久的时间呢?
先来看3-sum程序:
public class ThreeSum {
// Do not instantiate.
private ThreeSum() { }
/**
* Prints to standard output the (i, j, k) with {@code i < j < k}
* such that {@code a[i] + a[j] + a[k] == 0}.
*
* @param a the array of integers
*/
public static void printAll(int[] a) {
int n = a.length;
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) {
StdOut.println(a[i] + " " + a[j] + " " + a[k]);
}
}
}
}
}
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) {
In in = new In(args[0]);
int[] a = in.readAllInts();
Stopwatch timer = new Stopwatch();
int count = count(a);
StdOut.println("elapsed time = " + timer.elapsedTime());
StdOut.println(count);
}
}
通过这个例子,可知对于大多数程序,得到其运行时间的数学模型所需的步骤如下:
(1)确定输入模型,定义问题的规模
(2)识别内循环
(3)根据内循环的操作确定成本模型
(4)对于给定的输入,判断这些操作的执行频率。
例如,二分查找的输入模型是大小为N的数组a[],内循环是一个while循环中的所有语句,成本模型是比较两个数组元素的值。
2.增长数量级的分类及2-sum
3-sum就是一个典型的立方级别的例子,而指数级别的算法非常慢,不可能用它们解决大规模的问题。下面是典型的增长数量级函数图像:
在上面平方级别的代码其实就是2-sum代码,将3-sum.count()中关于k的循环和a[k]去掉即可得到一个双层循环来检查所有的整数对。还可以对2-sum再改进,在线性对数级别解决2-sum问题,改进后的算法思想是当且仅当-a[i]存在于数组中(且a[i]非零)时,a[i]存在于某个和为0的整数对之中。要解决这个问题,我们首先将数组排序(为二分查找做准备),然后对于数组中的每个a[i],使用BinarySearch中的rank()方法对-a[i]进行二分查找。这个条件测试覆盖了三种情况:
- 如果二分查找不成功则会返回-1,不会增加计数器的值
- 如果二分查找返回的j>i,就有a[i]+a[j]=0,增加计数器的值
- 如果二分查找返回的j在0和i之间,也有a[i]+a[j]=0,但不能增加计数器的值,以避免重复计数。
这样得到的结果和平方级别的算法结果相同,时间大幅减少。代码如下:
public class TwoSumFast{
public static int count(int[] a){
//计算和为0的整数对的数目
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));
}
}
3.3-sum问题的快速算法
和刚才一样,我们假设所有整数均不同。当且仅当-(a[i]+a[j])在数组中时,整数对(a[i]和a[j])为某个和为0的三元组的一部分。下面代码会将数组排序并进行N(N-1)/2次二分查找,每次查找时间都和logN成正比,因此总运行时间和成正比。
public class ThreeSumFast {
// Do not instantiate.
private ThreeSumFast() { }
// returns true if the sorted array a[] contains any duplicated integers
private static boolean containsDuplicates(int[] a) {
for (int i = 1; i < a.length; i++)
if (a[i] == a[i-1]) return true;
return false;
}
public static void printAll(int[] a) {
int n = a.length;
Arrays.sort(a);
if (containsDuplicates(a)) throw new IllegalArgumentException("array contains duplicate integers");
for (int i = 0; i < n; i++) {
for (int j = i+1; j < n; j++) {
int k = Arrays.binarySearch(a, -(a[i] + a[j]));
if (k > j) StdOut.println(a[i] + " " + a[j] + " " + a[k]);
}
}
}
public static int count(int[] a) {
int n = a.length;
Arrays.sort(a);
if (containsDuplicates(a)) throw new IllegalArgumentException("array contains duplicate integers");
int count = 0;
for (int i = 0; i < n; i++) {
for (int j = i+1; j < n; j++) {
int k = Arrays.binarySearch(a, -(a[i] + a[j]));
if (k > j) count++;
}
}
return count;
}
public static void main(String[] args) {
In in = new In(args[0]);
int[] a = in.readAllInts();
int count = count(a);
StdOut.println(count);
}
}
综上,2-sum算法的运行时间数量级是,2-sumfast是 ,3-sum是,3-sumfast是 。
4.内存
一个对象所用的内存量一般是16字节,这些开销包括一个指向对象的类的引用、垃圾收集信息以及同步信息。典型对象的内存需求如下图:
例如,一个含有N个整数的链表类型的栈需要使用(32+64N)字节,包括Stack对象的16字节对象开销,引用类型实例变量8字节,int型实例变量4字节,4个填充字节,每个元素需要64字节,包括Node对象的40字节,整数类型的24字节。
下面是Java中各种类型的数组对内存的典型需求:
下面是字符串及子字符串的内存开销:
当通过new创建对象时,系统会从堆内存的另一块特定区域为该对象分配所需的内存,而且所有对象会一直存在,直到对它的引用消失后被垃圾回收机制将它所占用的内存收回到堆中。
学习永不止步,继续加油~