假设现在我们需要求数组的一个和最大的子数组,比如对于数组{-1,1,3,-1},和最大的子数组就是{1,3}。我们来设计一下这样的算法。
一、暴力法
首先我们可能会想到使用暴力法两层循环来遍历数组求解,外层循环表示子数组的开始位置,内层循环表示子数组的结尾位置,一一遍历,最后得到最大的一个和以及子数组的开头和结尾,我们使用Java来实现:
public static int[] getMaxBonusByVlWay(int[] arr){
int maxBonus = arr[0], maxLeftLocation = 0, maxRightLocation = 0;
int sum;
int[] result = new int[3];
for(int i = 0; i<arr.length; i++){
sum = 0;
for(int j = i; j<arr.length; j++){
sum += arr[j];
if(sum > maxBonus){
maxBonus = sum;
maxLeftLocation = i;
maxRightLocation = j;
}
}
}
result[0] = maxLeftLocation;
result[1] = maxRightLocation;
result[2] = maxBonus;
return result;
}
这个算法的时间复杂度很明显为O(n^2),有没有其他时间复杂度小一点的思路呢?
二、分治法
事实上我们可以使用分治法来递归实现这一需求,至于如何分治呢?
1、分割。我们可以把这个范围为[low,high]的数组arr分为[low,mid]和[mid+1,high]两部分。
2、治理。对于每个需要求其最大子数组和的范围为[low,high]的数组arr,我们都把他分为[low,mid],[mid+1,high]。如果我们需要找到[low,high]这个范围的数组的最大子数组和,我们可以从三个地方来查找,然后取最大值。这个子数组可能位于[low,mid],[mid+1,high],以及跨越了mid,共三种可能,从中取最大值。直到分到只有一个元素,也就是low==high,我们直接把它的最大子数组和视为arr[low]。
3、合并。我们前面分到不能再分的之后,进行组合,最终找到最大的子数组和。
我们来实现这一思路:
//计算出跨mid的一个最大连续子数组和
private static int[] getMaxBonusCrossingMid(int[] arr, int low, int mid, int high){
if(low>mid||mid>high||low<0||high>arr.length)
throw new IllegalArgumentException("非法参数");
int[] result = new int[3];
int leftSum = Integer.MIN_VALUE;
int sum = 0;
int maxLeftLocation = 0;
for(int i = mid;i>=low; i--){
sum+=arr[i];
if(sum>leftSum){
leftSum = sum;
maxLeftLocation = i;
}
}
int rightSum = Integer.MIN_VALUE;
int maxRightLocation = 0;
sum = 0;
for(int i = mid+1 ;i<=high ;i++){
sum+=arr[i];
if(sum>rightSum){
rightSum = sum;
maxRightLocation = i;
}
}
result[0] = maxLeftLocation;
result[1] = maxRightLocation;
result[2] = leftSum + rightSum;
return result;
}
//递归计算一个数组的一个最大连续子数组和
public static int[] getMaxBonus(int[] arr, int low, int high){
if(low>high)
throw new IllegalArgumentException("非法参数");
if(low == high){
int[] result = new int[3];
result[0] = low;
result[1] = high;
result[2] = arr[low];
return result;
}
int[] leftResult = new int[3];
int[] rightResult = new int[3];
int[] crossingResult = new int[3];
int mid = (low + high) >> 1;
leftResult = getMaxBonus(arr, low, mid);
rightResult = getMaxBonus(arr, mid+1, high);
crossingResult = getMaxBonusCrossingMid(arr, low, mid, high);
if(leftResult[2]>rightResult[2]&&leftResult[2]>crossingResult[2])
return leftResult;
else if(rightResult[2]>leftResult[2]&&rightResult[2]>crossingResult[2])
return rightResult;
else
return crossingResult;
}
算法的关键在于,每当我们把一个范围为[low,high]的数组分为[low,mid],[mid+1,high],其中mid=(low+high)/2之后,我们需要从[low,mid],[mid,high]以及跨过mid的三个范围分别找到和最大的子数组,选出和最大的那个。这个算法很明显是二分递归,时间复杂度为O(nlgn)。时间复杂度明显优于前面的暴力求解法。
三、扫描法
遍历,之前的是负和抛弃之前的结果,重新积累,期间保留最大值,时间复杂度O(n):
public int getMaxBonus(int[] array) {
int sum = array[0];
int result = Integer.MIN_VALUE;
for(int i = 1; i < array.length; i++){
if(sum>0)
sum+=array[i];//前面的大于0,就加上面前的 否则舍弃
else
sum = array[i];
if(sum>result)
result = sum;
}
return result;
}