最近在学数据结构,书看的是《数据结构与算法分析 C语言描述》Mark Allen Weiss的中译本。
第二章中的最大子序列和问题:
给定整数A1,A2,......AN(可能有负数),求∑(k=i~j) Ak的最大值(为方便起见,如果所有整数均为负数,则最大子序列和为0)。
看到问题后我自己先写了一个笨笨的算法(想了好久啊- - 跟书上的例程相比无论哪方面高下立判啊)。。。时间复杂度是O(N2)的,记为Algorithm0。书上给出的四个算法分别记为Algorithm 1~4,时间复杂度分别为O(N3), O(N2), O(NlogN), O(N)(其中第三个递归真是惊呆我,本来以为递归效率都低的。。),记录当N分别为10,100,1000,2000,5000,10000,20000,50000等时的运行时间。N达到50000以上的时候不论哪个算法都显示停止工作了。。。估计内存的关系吧。。具体原因也不明白。。
具体代码如下,init()调用rand()产生(大小为-9~10)随机数并初始化至数组,通过修改#define SIZE的值改变N的大小,输出10次结果。由于每次显示数组的内容耗费时间长,可以把show()函数调用注释掉来只输出最大子序列和的结果。运行时间嘛。。最小好像就0.063, 0.078啥的,不过大体还是可以反映算法之间的优劣。
主程序:
/* test.c */
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include "Algorithms.h"
#define SIZE 50000
void init(int (*p) [SIZE],int n);
void show(const int *p,int n);
int main(void)
{
int ans;
int i;
int source[10][SIZE];
init(source,10);
printf("N= %d\n\n",SIZE);
for(i=0;i<10;i++)
{
ans=MaxSubsequenceSum4(source[i],SIZE);
show(source[i],SIZE); // 把这句注释掉可不显示具体序列只显示结果
printf("%d\n",ans);
}
return 0;
}
void show(const int *p,int n)
{
int i=0;
for(i=0;i<n;i++)
{
printf("%d\t",p[i]);
}
putchar('\n');
return;
}
void init(int (*p)[SIZE],int n)
{
int i,j;
srand((int)time(0));
for(i=0;i<n;i++)
{
for(j=0;j<SIZE;j++)
p[i][j]=10-rand()%20;
}
return;
}
算法:
/* Algorithms.c */
#include "Algorithms.h"
static int MaxSubSum(const int *a,int left,int right);
static int Max3(int a,int b,int c);
int MaxSubsequenceSum0(const int *p,int n)
{
int i,j,sum=0,ans=0,ThisSum=0;
_Bool last_is_pos=0;
for(i=0;i<n;i++)
{
if(p[i]<0||last_is_pos)
{
last_is_pos=0;
continue;
}
else
{
sum=p[i];
last_is_pos=1;
}
for(j=i+1,ThisSum=sum;j<n;j++)
{
ThisSum+=p[j];
if(p[j]>0)
{
sum= sum>ThisSum?sum:ThisSum;
}
}
ans=ans>sum?ans:sum;
ThisSum=0;
}
return ans;
}
int MaxSubsequenceSum1(const int *p,int n)
{
int ThisSum,MaxSum,i,j,k;
MaxSum=0;
for(i=0;i<n;i++)
{
for(j=i;j<n;j++)
{
ThisSum=0;
for(k=i;k<=j;k++)
ThisSum+=p[k];
if(ThisSum>MaxSum) MaxSum=ThisSum;
}
}
return MaxSum;
}
int MaxSubsequenceSum2(const int *p,int n)
{
int ThisSum,MaxSum,i,j;
MaxSum=0;
for(i=0;i<n;i++)
{
ThisSum=0;
for(j=i;j<n;j++)
{
ThisSum+=p[j];
if(ThisSum>MaxSum) MaxSum=ThisSum;
}
}
return MaxSum;
}
int MaxSubsequenceSum3(const int *p,int n)
{
return MaxSubSum(p,0,n-1);
}
int MaxSubsequenceSum4(const int *p,int n)
{
int MaxSum=0,ThisSum=0;
int i;
for(i=0;i<n;i++)
{
ThisSum+=p[i];
if(ThisSum>MaxSum)
MaxSum=ThisSum;
else if(ThisSum<0)
ThisSum=0;
}
return MaxSum;
}
static int MaxSubSum(const int *a,int Left,int Right)
{
int MaxLeftSum, MaxRightSum;
int MaxLeftBorderSum, MaxRightBorderSum;
int LeftBorderSum, RightBorderSum;
int Center, i;
if(Left==Right) //Base case
{
if(a[Left]>0)
return a[Left];
else
return 0;
}
Center=(Left+Right)/2;
MaxLeftSum=MaxSubSum(a,Left,Center);
MaxRightSum=MaxSubSum(a,Center+1,Right);
MaxLeftBorderSum=0;LeftBorderSum=0;
for(i=Center;i>=Left;i--)
{
LeftBorderSum+=a[i];
if(LeftBorderSum>MaxLeftBorderSum)
MaxLeftBorderSum=LeftBorderSum;
}
MaxRightBorderSum=0;RightBorderSum=0;
for(i=Center+1;i<=Right;i++)
{
RightBorderSum+=a[i];
if(RightBorderSum>MaxRightBorderSum)
MaxRightBorderSum=RightBorderSum;
}
return Max3(MaxLeftSum,MaxRightSum,MaxLeftBorderSum+MaxRightBorderSum);
}
static int Max3(int a,int b,int c)
{
int ans;
ans=a>b?a:b;
ans=ans>c?ans:c;
return ans;
}
头文件:
/* Algorithms.h */
int MaxSubsequenceSum0(const int *p,int n);
int MaxSubsequenceSum1(const int *p,int n);
int MaxSubsequenceSum2(const int *p,int n);
int MaxSubsequenceSum3(const int *p,int n);
int MaxSubSequenceSum4(const int *p,int n);
运行结果如下(运行时间单位为s):
元素个数N | 算法0 O(N2) | 算法1 O(N3) | 算法2 O(N2) | 算法3 O(NlogN) | 算法4 O(N) | |
---|---|---|---|---|---|---|
10 | 0.063 | 0.047 | 0.063 | 0.063 | 0.063 | |
100 | 0.063 | 0.063 | 0.063 | 0.063 | 0.063 | |
1000 | 0.063 | 4.969 | 0.078 | 0.063 | 0.063 | |
2000 | 0.094 | 39.051 | 0.141 | 0.063 | 0.063 | |
5000 | 0.313 | 609.464 | 0.453 | 0.063 | 0.063 | |
10000 | 1.313 | ------------ | 1.547 | 0.078 | 0.063 | |
20000 | 5.704 | ----------- | 5.860 | 0.094 | 0.063 | |
30000 | 13.220 | ----------- | 13.111 | 0.109 | 0.063 | |
50000 | 37.879 | ----------- | 36.254 | 0.141 | 0.063 |
大体上还是验证了这几个算法的效率区别,也算第一次切身体会到算法的重要性,虽然只是一个简单的问题,同一个问题不同解决方案,结果相同但效率的差别是惊人的。不过递归那个不是很理解哪!另外这也只是一个粗略的测试,这个过程本身肯定有很多问题。。。还是继续看书吧= =
关于运行时间中的对数:
书中的翻译是这样的:如果一个算法用常数时间(O(1))将问题的大小削减为其一部分(通常是1/2),那么该算法就是O(logN)。另一方面,如果使用常数时间只是把问题减少一个常数(如将问题减少1),那么这种算法就是O(N)的。
翻译很拗口,不过好在下面给出了三个例子:对分查找、欧几里得算法和取幂运算,再结合前面的最大子序列和问题,我的理解是这样的:无论是用循环还是递归,程序每次执行时都将变量减半(上面说的削减为一部分),并且循环和递归之外的语句是常数阶复杂度O(1),也就是有确定的执行次数,这样的程序往往是对数阶的。
至于原理,直观上来讲,比如开始变量为N,每次变为1/2,需要logN次就可以让变量从N变到1,此时也就是基准情况,所以执行次数就是logN了。更加准确的分析是使用递推公式,例如上面的最大子序列和,递归之外的语句复杂度为O(N),设求解大小为N时时间为T(N),可以列出递推公式:
T(1)=1;
T(N)=2T(N/2)+O(N);
把这里的O(N)用N代替可以算出T(N)=NlogN+N=O(NlogN)
当然这个程序是所谓的“分治”方法,复杂度为NlogN的,并不是logN,但分析方法都是类似的,即通过递推公式。
例如:最后的取幂运算问题,算法如下:
long int pow(long int x, unsigned int N)
{
if(N==0) return 1;
if(N==1) return X;
if(IsEven(N)) return Pow(X*X,N/2);
else return pow(X*X,N/2)*X;
}
这里面如果用乘法的次数衡量运行时间,或者连判断也算上,总之每次递归内部都是O(1),列出的递推式:
T(1)=1;
T(N)=T(N/2)+O(1);
把O(1)当1来算,可以得出来T(N)=logN+1=O(logN)。
不过这里通过递推得到公式都是写出前几项归纳的。。。至于真正怎么去算这个公式是数学问题。。书里预言了第七章会讲。。