按照阅读顺序记录笔记
算法是为求解一个问题需要遵循的、被清楚地指定的简单指令集和,确定某个算法是正确的,之后确定算法需要多少时间或空间就是最重要的了。
数学基础
时间复杂度:一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n))为算法的渐进时间复杂度(O是数量级的符号 ),简称时间复杂度。
也就是当N趋于无穷,lim(n->∞) T(N)/f(N)的比值情况。
法则
- T(n,m) = T1(n) + T2(n) = O (max ( f(n), g(m) )
- T(n,m) = T1(n) * T2(m) = O (f(n) * g(m))
- 对于任意常数k, log^k(N) = O(N),对数的无论什么幂次也不比N快!
- 如果T(N)是k次多项式,去掉常数和低阶项仍等阶,T(N) 等阶于 N^k
复杂度和时间效率关系
复杂度与时间效率的关系:
1/n<c < log2n < n < n*log2n < n2 < n3 < 2n < 3n < n! (c是一个常量)
|--------------------------|--------------------------|-------------|
较好 一般 较差
要分析的问题
最坏时间:算法的时间复杂度不仅与语句频度有关,还与问题规模及输入实例中各元素的取值有关。一般不特别说明,讨论的时间复杂度均是最坏情况下的时间复杂度。这就保证了算法的运行时间不会比任何更长。
最坏时间规定了界限,平均情况提供不了,而且平均情况计算起来更难
运行时间计算
规定:不存在特定的时间单元,抛弃前导常数,抛弃低阶项,计算大O的运行时间。
- 找到执行次数最多的语句
- 计算语句执行次数的数量级
- 用大O来表示结果
一般法则
- for循环法则:
运行时间
=for循环内语句运行时间
*迭代次数
for(int i=0;i<n;i++)
sum++;
//语句内运行时间为1,迭代次数n,所以运行时间=O(N)
- 嵌套for循环法则(for循环法则的推论)
一组嵌套循环内部语句的总运行时间
=该语句运行时间
*所有for循环乘积大小
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
k++;
//语句运行时间为1,for乘积为n*n,所以运行时间=O(N^2)
for(int i=0;i<n;i++)
for(int j=0;j<n;j*=2)
k++;
//该语句运行时间1,for乘积n*logn,所以运行时间=O(NlogN)
- 顺序语句
将各个语句求和即可,意味着最大值就是运行时间
for(int i=0;i<n;i++)
sum++;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
k++;
//运行时间是T(N) = n+ n^2=O(N^2)
分析的基本策略是inside to outside,如果有函数调用,那么首先分析函数调用。
最大子数列和问题
算法1
int MAX1(const int A[],int n)
{
int sum,maxsum,i,j,k;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
{
sum =0;
for(k=i;k<=j;k++)
{
sum+= A[k];
}
if(sum>maxsum)
maxsum = sum;
}
return maxsum;
}
运行时间复杂度为O(N^3),这种方法就比较笨b了,它尝试了每一种情况
int MAX2(const int A[],int n)
{
int sum,maxsum,i,j;
for(i=0;i<n;i++)
{
sum=0;
for(j=i;j<n;j++)
{
sum+=A[j];
if(sum>maxsum)
maxsum= sum;
}
}
return maxsum;
}
这种方法时间复杂度为O(N^2),它只是在MAX1方法基础上减了一个for循环,本质并没有区别,但速度要快很多(数据大的话)
分治思想
int Max(int a,int b, int c)
{
int t1 = a>b?a:b;
int t2 = t1>c?t1:c;
return t2;
}
static
int MaxSubSum(const int A[],int left,int right)
{
int maxlsum,maxrsum;
int maxlbs,maxrbs;
int lbs,rbs;
int center,i;
if(left == right)
if(A[left]>0)
return A[left];
else
return 0;
center = (left+right)/2;
maxlsum = MaxSubSum(A,left,center);
maxrsum = MaxSubSum(A,center+1,right);
maxlbs=0; lbs=0;
for(i=center;i>=left;i--)
{
lbs+=A[i];
if(lbs>maxlbs)
maxlbs=lbs;
}
maxrbs=0; rbs=0;
for(i=center+1;i<=right;i++)
{
rbs+=A[i];
if(rbs>maxrbs)
maxrbs=rbs;
}
return Max(maxlsum,maxrsum,maxlbs+maxrbs);
}
int MAX3(const int A[],int n)
{
return MaxSubSum(A,0,n-1);
}
时间复杂度:
Base case的时间复杂度为O(1);
两个for循环时间复杂度为O(N);
其他行复杂度都是常量级,omit
两个递归调用,求解大小为N/2的子序列问题(N为偶数假设),所以这部分花费2T(N/2)时间
总共花费:T(N)=2T(N/2)+O(N) ,用N简化O(N),则T(N)=2T(N/2)+N,由于T(1)=1,则T(2)、T(N)都可以推导出来,T(N)=Nlog N+ N =O(NlogN)
关于这个问题分治算法思想:分治算法思想
(其他讲的云里雾里)
算法4
int MAX4(const int A[],int n)
{
int sum,maxsum, i;
sum = maxsum =0;
for(i=0;i<n;i++)
{
sum+=A[i];
if(sum>maxsum)
maxsum = sum;
else if(sum<0)
sum=0;
}
return maxsum;
}
这个算法很收膝盖,时间复杂度为O(N)
测试时间:
const int SIZE = 2000;
clock_t starttime,endtime;
srand((int)time(0));
int arr[SIZE];
for(int i=0;i<SIZE;i++)
{
arr[i]=rand()%SIZE;
}
//algorithm 1
starttime = clock();
cout<<MAX1(arr,SIZE)<<endl;
endtime = clock();
cout<<"Use time: "<<(double)(endtime - starttime)/CLOCKS_PER_SEC<<"s"<<endl;
//algorithm 2
starttime = clock();
cout<<MAX2(arr,SIZE)<<endl;
endtime = clock();
cout<<"Use time: "<<(double)(endtime - starttime)/CLOCKS_PER_SEC<<"s"<<endl;
//3
starttime = clock();
cout<<MAX3(arr,SIZE)<<endl;
endtime = clock();
cout<<"Use time: "<<(double)(endtime - starttime)/CLOCKS_PER_SEC<<"s"<<endl;
//4
starttime = clock();
cout<<MAX4(arr,SIZE)<<endl;
endtime = clock();
cout<<"Use time: "<<(double)(endtime - starttime)/CLOCKS_PER_SEC<<"s"<<endl;
测试数据量2000:
测试数据量10000(alogorithm1 is dead)
测试数据量:100,000(algorithm2 is dead)
可以看到O(N)与O(NlogN)相差无几
来个100,000,000:
运行时间中的对数
如果一个算法用常数时间O(1),将问题的大小削减一半1/2,那么该算法是O(log N)。
如果一个算法使用常数时间只是把问题减少一个常数,那这种算法是O(N)
对分查找(二分查找),循环次数最多为[log(N-1) ] +2,运行时间为O(log N)
插入操作需要O(N)时间
欧几里得算法(找最大公因数)
unsigned int
Gcd(unsigned int M,unsigned int N)
{
unsigned int rem;
while(N>0)
{
rem = M%N;
M=N;
N=rem;
}
return M;}
可以证明,在两次迭代后余数最多为原来的一半,所以次数最多为2log N次=O(log N)
幂运算
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
可以通过X^8和 X^15等得出奇数幂和偶数幂与下一次分解计算的关系. 显然,乘法次数最多2log N次 = O(log N)
补:为什么是log N次呢,每次削减1/2,就好像128变为64、32、16、8、4、2、1,其实就是2^k=N,然后k就是次数,log N是以2为底(本书规定)