时间复杂度分析经典问题——最大子序列和
最大子序列和问题
最大子列和问题是非常经典的问题,基本上讲算法的书都会将这个例子,用此例题来讲解算法时间复杂度的重要性,对比不同算法的时间复杂度。最大子列和问题如下:给定整数序列A1,A2,A3,A4,...,An(可能存在负数),求A(i)+A(i+1)+........+A(j)的最大值(无法输入公式),请看下图:
注:为了方便起见,如果所有的整数均为负数,则最大的子序列和为0
算法的运行时间
这个问题之所以有如此的吸引力,主要是因为存在求解它的很多算法,而且这些算法的性能又差异很大。我们将讨论求解该问题的四种算法。这四种算法的运行时间如下表所示:(算法1是O(N^3),图中写错了)
- 表中的几个重要的情况值得注意。对于小量的输入,算法可以在眨眼之间的完成,因而如果只是小量输入的情况下,那么花费大量的时间去设计优秀的算法恐怕是不值得的。另一方面,随着业务,用户的增加,小量输入的情况可能会发生变化,哪些低效率的程序可能必须要进行重写。
- 其次,表中所给的时间不包括读入数据的所需要的时间,对于算法4,仅仅从磁盘读入数据所用的时间很可能在数量级上比求解问题所需的时间还要大。数据的读入一般是一个瓶颈,一旦数据读入,问题就会迅速解决。但是对于低效的算法,它必然要耗费大量的计算资源。因此,只要可能,使得算法足够有效而不至于成为问题的瓶颈是非常重要的。
我们还可以通过函数曲线来对这四种算法的时间复杂度函数进行分析,通过曲线我们清楚的可以看出O(nlgn)算法时间复杂度是介于O(n^2)的O(n)之间的,当然这也不难证明。在实际的情况中,当我们采用O(n^2)算法的时候,应该在仔细想想,能否将算法的时间复杂度优化成O(nlgn),这对算法的性能提升也是非常巨大的,不妨要问,为什么不优化为O(n)呢?事实上,O(n)时间复杂度意味着只需要进行一次扫描,就能找到问题的解,在大部分的问题中,这是非常的困难的。
O(n^3)算法
1 #include<iostream> 2 #include<stdio.h> 3 using namespace std; 4 5 int MaxSubsequenceSum(int a[],int n); 6 7 int main(){ 8 //int a[6] = {-2, 11, -4, 13, -5, -2}; 9 int a[8] = {4, -3, 5, -2, -1, 2, 6, -2}; 10 printf("%d\n",MaxSubsequenceSum(a,8)); 11 } 12 13 int MaxSubsequenceSum(int a[],int n){ 14 int ThisSum, MaxSum; 15 MaxSum = 0; 16 for(int i = 0; i < n; i++){ 17 for(int j = i; j < n; j++){ 18 ThisSum = 0; 19 for(int k = i; k <= j; k++){ 20 ThisSum += a[k]; 21 } 22 if(ThisSum > MaxSum){ 23 MaxSum = ThisSum; 24 } 25 } 26 } 27 return MaxSum; 28 }
这是一种O(n^3)的解法,说实话,我是写不来这样高时间复杂度的算法,这个算法重复做了很多的无用的计算,强行将算法复杂化,经过简单的分析,直接可以求 ThisSum += a[k] 语句的次数,就能够得出它的时间复杂度:
O(n^2)算法
对上述的算法直接优化,我们发现最里面的循环是完全多余的,很过分的消耗了大量的时间,很容易就能得到下面的算法
1 int MaxSubsequenceSum(int a[],int n){ 2 int ThisSum, MaxSum; 3 MaxSum = 0; 4 for(int i = 0; i < n; i++){ 5 ThisSum = 0; 6 for(int j = i; j < n; j++){ 7 ThisSum += a[j]; 8 if(ThisSum > MaxSum){ 9 MaxSum = ThisSum; 10 } 11 } 12 } 13 return MaxSum; 14 }
相信大部分人首想想到的应该是这个算法把,这个算法性能只能说还行。但是,我们想到了O(n^2)的时候,应该多思考一下,能否将其转化为O(nlogn)呢?如果能的话,这将会极大的提高算法的性能。
O(nlogn)算法
如果没有O(n)算法的话,那么递归的威力就能体现出来了。这个算法采用的是分治策略,分治思想是把所求问题划分成两个大致相等的问题,然后递归的对它进行求解,这是分的思想,治的阶段是将两个子问题的解合并到一起,最后得到整个问题的解。
在这个问题中,最大的子序列和可能出现在三处,要么是序列的左半部分,要么是序列的右半部分,要么是跨越输入数据的中间左右部分都有,前面的两种情况可以用递归进行求解,第三种情况的最大子序列和可以通过求出前半部分的最大和以及后半部分的最大和而得到,我们可以通过下面的例子进行分析:
- 前半部分最大子序列和为6,
- 后半部分的最大子序列和为8。
- 前半部分包含最后一个元素的最大和是4,而后半部分包含第一个元素的的最大和是7,因此跨越两部分的最大和是11,这是最大的子列和。
这个算法的源码有点复杂,仔细读几遍。
1 int MaxSubSum(int A[], int Left, int Right){ 2 int MaxLeftSum, MaxRightSum; 3 int MaxLeftBorderSum, MaxRightBorderSum; 4 int LeftBorderSum, RightBorderSum; 5 int Center; 6 if(Left == Right){ 7 if(A[Left] > 0){ 8 return A[Left]; 9 }else{ 10 return 0; 11 } 12 } 13 14 Center = (Left + Right) / 2; 15 MaxLeftSum = MaxSubSum(A, Left, Center); //递归求解左半部分的最大和 16 MaxRightSum = MaxSubSum(A, Center + 1, Right); //递归求解右半部分的最大和 17 18 MaxLeftBorderSum = 0; 19 LeftBorderSum = 0; 20 for(int i = Center; i >= Left; i--){ 21 LeftBorderSum += A[i]; 22 if(LeftBorderSum > MaxLeftBorderSum){ 23 MaxLeftBorderSum = LeftBorderSum; 24 } 25 } 26 27 MaxRightBorderSum = 0; 28 RightBorderSum = 0; 29 for(int i = Center+1; i <= Right; i++){ 30 RightBorderSum += A[i]; 31 if(RightBorderSum > MaxRightBorderSum){ 32 MaxRightBorderSum = RightBorderSum; 33 } 34 } 35 return Max3(MaxLeftBorderSum+MaxRightBorderSum,MaxLeftSum,MaxRightSum); 36 } 37 38 int Max3(int a, int b, int c){ 39 if(a>b){ 40 return a > c ? a : c; 41 }else{ 42 return b > c ? b : c; 43 } 44 } 45 46 int MaxSubsequenceSum(int a[],int n){ 47 return MaxSubSum(a, 0, n-1); 48 }
时间复杂度分析
有兴趣的同学可以参考网易公开课:麻省理工学院公开课:算法导论,第三集分治法,讲的非常详细,还有推导过程。
O(n)算法
1 int MaxSubsequenceSum(int a[],int n){ 2 int ThisSum = 0, MaxSum = 0; 3 for(int j = 0; j < n; j++){ 4 ThisSum += a[j]; 5 if(ThisSum > MaxSum){ 6 MaxSum = ThisSum; 7 }else if (ThisSum < 0){ 8 ThisSum = 0; //ThisSum < 0,说明跨越a[j]不能使序列和变大 9 } 10 } 11 return MaxSum; 12 }
这个算法的效率非常的高,又被称为在线处理算法,算法只需要扫描一遍序列,就能找到最大的子序列和,它的技巧就是一旦A[i]被读入并被处理,它就不再需要被记忆。不仅如此,在任意时刻,算法都能够对它已经读入的数据给出正确的答案。具有这种特性的算法叫做联机算法。仅需要常量的空间并以线性时间运算的联机算法集合是完美的算法。