最大子数组:
在一个数组A中寻找和最大的非空连续子数组。我们称这样的连续子数组为最大子数组。
注:1、我们通常说“一个最大子数组”而不是“最大子数组”,因为可能有多个子数组达到最大和。
2、只有当数组中包含负数时,最大子数组问题才有意义。如果所有元素都是非负的,整个数组的和肯定是最大的。
使用分治策略的求解方法
将数组A划分为两个规模尽量相等的子数组,找到中央位置 mid,所以数组A中任何连续子数组A [i … j ]所处位置必然是以下情况:
- 位于前半部分
A[low…mid]
中,因此low ≤ i ≤ j ≤ mid
- 位于后半部分
A[mid+1…high]
中,因此mid < i ≤ j ≤ high
- 跨越了中点,因此
low ≤ i ≤ mid < j ≤ high
我们可以递归的求解 A[low…mid]
和 A[mid+1…high]
的最大子数组,因为这两个子问题仍然是最大子数组问题,符合递归条件。剩下的全部工作就是寻找跨中点的子数组,然后在三种情况中取和最大者。
在解决跨中点的子数组时,其中 low ≤ i ≤ mid 且 mid < j ≤ high
因此,我们只需找出中点前后两部分的最大子数组,然后将其合并即可。
以下面数组为例,分别以递归算法和暴力算法来实现:
伪代码如下:
FIND-MAX-CROSSING-SUBARRAY(A,low,mid,high)
1 left-sum = -∞ //初始化变量
2 sum = 0
3 for i = mid downto low//左半部分
4 sum = sum + A[i]
5 if sum > left-sum
6 left-sum = sum //更新left-sum为这个子数组的和
7 max-left = i //更新变量max-left来记录当前下标i
8 right-sum = -∞//右半部分
9 sum = 0
10 for j = mid + 1 to high
11 sum = sum + A[j]
12 if sum > right-sum
13 right-sum = sum
14 max-right = j
15 return(max-left,max-right,left-sum + right-sum) //返回左右边界i和j与最大子数组总和
FIND-MAXIMUM-SUBARRAY(A,low,high)
1 if high == low //检测子数组只有一个元素的情况
2 return(low,high,A[low])
3 else mid = ⌊(low + high) / 2 ⌋ //划分子数组
/**分别求解左右部分最大子数组**/
4 (left-low,left-high,left-sum) =
FIND-MAXIMUM-SUBARRAY(A,low,mid)
5 (right-low,right-high,right-sum) =
FIND-MAXIMUM-SUBARRAY(A,mid+1,high)
/**完成合并**/
6 (cross-low,cross-high,cross-sum) =
FIND-MAX-CROSSING-SUBARRAY(A,low,mid,high)//跨越中点最大子数组
/**三者互相比较,找出和最大的(子数组)**/
7 if left-sum >= right-sum and left-sum >= cross-sum
8 return(left-low,left-high,left-sum)
9 elseif right-sum >= left-sum and right-sum >= cross-sum
10 return(right-low,right-high,right-sum)
11 else return(cross-low,cross-high,cross-sum)
递归算法实现:
public class Subarray {
//如果最大子数组横跨左右部分数组
public static int[] FindMaxCrossingSubarray(int[] A, int low, int mid, int high) {
int[] result = new int[3];
//从中间点向左遍历,找出左半部分过中点的最大子数组
int sum = 0;
int i;
for (i=mid;i>=low;i--) {
sum = sum + A[i];
if (sum>result[2]) {
result[2] = sum;
result[0] = i;
}
}
//从中间往右遍历,找出右半部分过中点的最大子数组
sum = 0;
result[2] = 0;
for (i=mid;i<=high;i++) {
sum = sum + A[i];
if (sum>result[2]) {
result[2] = sum;
result[1] = i;
}
}
//将左右两个子数组组合
result[2] = 0;
for (i = result[0];i<=result[1];i++) {
result[2] = result[2] + A[i];
}
return result;
}
//将数组分成左右两个部分,依次计算左数组、右数组的最大子数组,再计算横跨左右部分的中间子数组,选择最大的输出
public static int[] FindMaximumSubarray(int[] A, int low, int high) {
int[] result = new int[3];
int[] result_left = new int[3];
int[] result_right = new int[3];
int[] result_cross = new int[3];
int mid = (int)(low+high)/2;
//基本情况:数组中只有一个元素,则返回该数组
if(low==high) {
result[0]=low;
result[1]=high;
result[2]=A[low];
return result;
}
//递归情况:数组中多于一个元素,依次计算三种情况下的最大子数组
else {
result_left = FindMaximumSubarray(A, low, mid);
result_right = FindMaximumSubarray(A, mid+1, high);
result_cross = FindMaxCrossingSubarray(A, low, mid, high);
//比较三种情况的最大子数组,选择最大的输出
if (result_left[2]>result_right[2] && result_left[2]>result_cross[2]) {
return result_left;
}
else if (result_right[2]>result_left[2] && result_right[2]>result_cross[2]) {
return result_right;
}
else {
return result_cross;
}
}
}
public static void main(String[] args) {
int[] result = new int[3];
int[] A = {13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7};
result = FindMaximumSubarray(A, 0, 15);
System.out.print("最大子数组为 : ");
for(int i=result[0]; i<=result[1];i++) {
System.out.print(A[i]+" ");
}
System.out.print("\n最大子数组和 : "+result[2]);
}
}
运行结果如下:
暴力实现:
public class SubArray {
public int bruteMethod(int[] A) {
int maxResult = A[0];
int maxTemp = 0;
;
for (int i = 0; i < A.length; i++) {
for (int j = i; j < A.length; j++) {
for (int k = i; k <= j; k++) {
maxTemp += A[k];
}
if (maxTemp > maxResult)
maxResult = maxTemp;
maxTemp = 0; // 完成一个子序列求和后,重新赋值为0
}
}
return maxResult;
}
public static void main(String[] args) {
MaxSubArray test = new MaxSubArray();
int[] A = { 13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7 };
System.out.println("暴力法求解数组A的最大子数组和为:" + test.bruteMethod(A));
}
}
运行结果如下:
讨论两种算法的性能:
暴力算法的复杂度是O(n2)
,递归算法是O ( n lg ( n ) )
,先计算出交叉点,再经过大规模测试数据,由于由于n0的存在,在 [0 , n0] 间是暴力法较优,在 [n0 , +∞] 是递归法较优。
- 若n0小于等于原交叉点X0时,交叉点不改变,因为在原交叉点[n0 , x0]仍是递归,且此时递归处理的效率不如暴力算法,从 x0 之后递归的效率才超出。
- 若n0大于原交叉点x0时,交叉点更新为n0 ,此时改良后的算法在 [0 , x0] 时暴力较优,在 [x0 , n0] 之间,虽然递归效率更高,但是由于仍然使用暴力法,所以并没有性能交叉,而在 [n0 , +∞] 之后,采用了递归法,且由于x0 < n0 ,此时递归效率高于暴力算法,所以性能交叉点变为 n0