前言:一周一算法,任道而重远!
问题描述:给定一个数组A[0,…,n-1],求A的连续子数组,使得该子数组的和最大。
比如:数组:A =[ 1, -2, 3, 10, -4, 7, 2, -5]
最大子数组:3, 10, -4, 7, 2
1:直接求取
思想:先从第一个元素开始向后累加, 每次累加后与之前的和比较,保留最大值, 再从第二个元素开始向后累加,保留第一个元素和第二个元素的最大值,依次循环调用,
直接对于每一个数求其最大连续子数组然后比较每个数的最大连续子数组的大小。
1 public class ViolenceMethod { 2 public static void maxSubarray(int a[] ){ 3 int i,j; 4 int maxSub = 0; 5 for( i = 0; i <a.length ; i++){ 6 int curSub = 0; 7 for( j = i ; j < a.length ; j++){ 8 curSub+= a[j]; 9 if (maxSub < curSub) 10 maxSub = curSub; 11 } 12 } 13 System.out.println("最大子数组的和 : " +maxSub); 14 }
时间复杂度 :时间复杂度为 O( n2 ) 发现时间复杂度较高。(因为对于每一个元素每次都必须遍历所有的元素)
2:分治的方法
基本思想:
基本思想采用分而治之的想法。时间复杂度为Nlogn
可以将一个数组分为两部分,左边子数组和右边子数组
那么最大的子数组只可能有三种情况
1:最大子数组位于左边(可以继续采用递归)
2:最大子数组位于右边(可以继续采用递归)
3:最大子数组跨越左右两边
这里面最复杂的情况就是跨越两边的情况,并且跨越中点的情况,必须包含A[mid]元素,也就是说任何跨越中点元素的最大子数组的由 A[ i....mid ] 和A[mid+1......j ] 组成
-------------------------------------------下面图片引自与算法导论---------------------------------------
因此可以写出求出跨越中点元素A[mid] 的伪码:
1 Find_Max_SubArray_cross(A,low ,mid ,right){ 2 //先处理左边的元素 定义sum为做数组左边的所有元素的和 ,max_left表示目前为止找到的最大和 3 max_left=-0000 4 sum = 0 5 for i = mid downto left 6 sum += A[i] 7 if sum > max_left 8 max_left = sum 9 int m = i // recoder maxleftsubscript i 10 11 max_right = -10000 //任何可以表示无穷大的数即可 12 sum = 0 13 for j = mid+1 to right 14 sum += A[j] 15 if sum > max_right 16 int n = j //recoder max rightsubscript j 17 return (m,n,max_left+max_right) 18 }
1 public static int maxSubArray2(int[] a, int left , int right){ 2 int MaxLeftSum , MaxRightSum ; //左右边和最大值 3 int MaxLeftBorderSum, MaxRightBorderSum ; //含中间边界的最大左边或者右边的子序列和 最终要求值 4 int LeftWithBoderNowSum,RightWithBorderNowSum; //含中间边界的左右部分的当前的最大子数组的和 5 int i , center; 6 7 //递归的边界条件 8 if (left == right) 9 if(a[left]>0) 10 return a[left]; 11 else 12 return 0; 13 14 //首先求含中间边界的左右部分的最大值 15 //实际上是左数组的最大后缀和右数组的最大前缀的和 16 center = (left+right)/2; 17 MaxLeftBorderSum = 0; 18 LeftWithBoderNowSum = 0; 19 for(i = center; i>= left; i--){ 20 LeftWithBoderNowSum += a[i]; 21 if(LeftWithBoderNowSum > MaxLeftBorderSum) 22 MaxLeftBorderSum = LeftWithBoderNowSum; 23 } 24 25 MaxRightBorderSum = 0; 26 RightWithBorderNowSum = 0; 27 for ( i = center; i <= right; i++) { 28 RightWithBorderNowSum +=a[i]; 29 if (RightWithBorderNowSum > MaxRightBorderSum) 30 MaxRightBorderSum = RightWithBorderNowSum ; 31 } 32 33 //求最大子数组在左边的情况(一直递归调用上面的maxSubArray2() ) 34 MaxLeftSum = maxSubArray2(a, left, center); 35 MaxRightSum = maxSubArray2(a, center+1, right); 36 37 //再返回三者的最大值 38 return Max3(MaxLeftSum, MaxRightSum, MaxRightBorderSum+MaxLeftBorderSum); 39 }
1 public static int Max3(int a, int b, int c){ 2 int max = a; 3 if (b > max) 4 max = b; 5 if (c > max) 6 max = c; 7 return max ; 8 }
时间复杂度:每次将所有的值分为两份,并且每次得出的值都需要比较(总体需要N次) 所以总的时间复杂度为 O(N logn)
详细时间复杂度的分析可以参考“算法导论”
3:线性表示方法
设计一个非递归的线性时间复杂度的计算方法
基本思想:
设定两个变量 maxSum表示最大子序列的和,cursum表示当前的的子序列的累积和
每加一个元素都比较maxSum和cursum,并且如果当前cursum<0 则直接将curSum = 0
1 public class LinearTimeMethod { 2 public static int MaxSubArray3(int[] a ){ 3 int curSum = 0; 4 int MaxSum = 0; 5 6 for (int i = 0; i < a.length; i++) { 7 curSum += a[i]; 8 if (curSum > MaxSum) 9 MaxSum = curSum ; 10 //如果累加和出现小于0的情况, 则和最大的子序列肯定不可能包含前面的元素, 这时将累加和置0,从下个元素重新开始累加 11 if (curSum < 0) 12 curSum = 0; 13 } 14 return MaxSum; 15 }
时间复杂度 :时间复杂度O(n)
4:动态调用
1 public class DynamicPlan { 2 //定义result为最大子数组的和 定义sum为当前子数组的和 3 public static int MaxSubArray4(int[] a){ 4 int result = a[0]; 5 int sum = a[0]; 6 for (int i = 1; i < a.length; i++) { 7 if (sum > 0) 8 sum +=a[i]; 9 else sum = a[i]; 10 11 if (sum > result) 12 result = sum; 13 14 } 15 return result; 16 }
上面的动态调用和线性方法类似,都是设定连个变量,一个为当前子数组的和 sum , 一个为最大的连续子数组的和result 。 不断的判断两者的大小。
并且返回最后较大的那个数result。 总体的时间复杂度为 O(n)