给定一个数组,其中有正数,也有负数,现在要求寻找其中连续的、和最大的子数组。
很容易想到的,可以暴力求解,把每个子数组都找出来,然后求和,进行比较。但是长度为n的数组拥有(1+n)*n/2个子数组,再对数组求和,时间复杂度显然是Ω(n^2),这是不能接受的。
所以这里利用分治的思想来求解。
分解时,我们将数组不停地等分,直至数组只剩下一个元素。
合并时,我们可以考虑一个数组的最大子数组可能有三种情况:
1.出现在左边的子数组中
2.出现在右边的子数组中
3.一部分在左边,一部分在右边
此时我们已知左右两个子数组的最大子数组,现在只需求出横跨两边的最大数组并进行比较,三者中最大的就是原数组的最大子数组。
举个栗子:可以想象,如果一个原数组为a1,a2,a3,a4,a5,a6,a7,a8,a9,a10
在合并阶段需要将先前分解为a1,a2,a3,a4,a5和a6,a7,a8,a9,a10的这两个子数组进行合并,此时的情况是已知a1,a2,a3,a4,a5和a6,a7,a8,a9,a10中的两个最大子数组,假设分别为a2,a3和a6,a7,a8,但是这二者之中一定包含原数组的最大子数组吗?答案是否定的,因为原数组的最大子数组有可能是a4,a5,a6,a7。所以,我们需要找到这样的既包含左子数组元素,又包含右子数组元素的子数组,并在这三个数组中寻找和最大的那个,那个就一定是原数组的最大子数组。
代码:
static class ChildArray {
int begin, end;
int sum;
public ChildArray () {}
public ChildArray (int begin, int end, int sum) {
this.begin = begin;
this.end = end;
this.sum = sum;
}
}
首先定义一个子数组类,用于记录数组的起始位置和终止位置,以及子数组和。
public static ChildArray findMaxCrossingSubArray (int[] a, int low, int mid, int high) {
//设置leftSum和rightSum用于记录左右两侧的数组和
int leftSum = Integer.MIN_VALUE, rightSum = Integer.MIN_VALUE;
int sum = 0;
//设置maxLeft和maxRight用于记录子数组左右头元素的位置
int maxLeft = mid, maxRight = mid + 1;
//从左子数组的最右边开始向左遍历,寻找最大子数组
for (int i = mid; i >= low; i--) {
sum += a[i];
if (sum > leftSum) {
leftSum = sum;
maxLeft = i;
}
}
sum = 0;
//从右子数组的最左边开始遍历,寻找最大子数组
for (int j = mid + 1; j <= high; j++) {
sum += a[j];
if (sum > rightSum) {
rightSum = sum;
maxRight = j;
}
}
ChildArray child = new ChildArray ();
child.begin = maxLeft;
child.end = maxRight;
child.sum = leftSum + rightSum;
return child;
}
这个findMaxCrossingSubArray()方法就是用来寻找横跨两边的最大子数组的。
代码:
public static ChildArray findMaximumSubarray (int[] a, int low, int high) {
ChildArray child = new ChildArray ();
ChildArray left = new ChildArray ();
ChildArray right = new ChildArray ();
ChildArray cross = new ChildArray ();
if (high == low) { // 若果数组中仅剩下一个元素,那么该一元素数组就是自身的最大子数组
child.begin = low;
child.end = high;
child.sum = a[low];
return child;
} else { // 若果数组中元素大于1,则继续分解,直至只剩下一个元素为止
//寻找到原数组中点,将原数组等分
int mid = (low + high) / 2;
left = findMaximumSubarray (a, low, mid);
right = findMaximumSubarray (a, mid + 1, high);
cross = findMaxCrossingSubArray (a, low, mid, high);
//对三者进行比较,寻找最大的那个
if (left.sum >= right.sum && left.sum >= cross.sum) {
return left;
} else if (cross.sum >= left.sum && cross.sum >= right.sum) {
return cross;
} else {
return right;
}
}
}
这个findMaximumSubarray()方法即可寻找到最大子数组。
带测试的完整代码:
public class Test0104 {
public static void main(String[] args) {
int[] a = {13,-3,-25,20,-3,-16,-23,18,20,-7,12,-5,-22,15,-4,7};
ChildArray childArray = vointFindMax (a);
int begin = childArray.begin;
int end = childArray.end;
System.out.println ("暴力解法:");
System.out.println ("最大子数组是第" + (begin + 1) + "个元素" + "到第" + (end + 1) + "个元素" + ",和是" + childArray.sum);
ChildArray childArray2 = findMaximumSubarray (a, 0, a.length - 1);
System.out.println ("分治解法");
System.out.println (childArray2.begin + " " + childArray2.end + " " + childArray2.sum);
}
public static ChildArray vointFindMax (int[] a) {
int max = a[0];
ChildArray child = new ChildArray ();
for (int i = 0; i < a.length; i++) { // i作为子数组起点
int temp = a[i]; // 设置局部变量用来记录每一段数组之和
for (int j = i + 1; j < a.length; j++) { // j作为子数组终点
temp += a[j]; // 循环记录每一段数组之和
if (max < temp) { // 比较max与子数组之和,若temp较大,则替换max并记录当前数组下标
max = temp;
child.begin = i;
child.end = j;
child.sum = max;
}
}
}
return child;
}
public static ChildArray findMaxCrossingSubArray (int[] a, int low, int mid, int high) {
//设置leftSum和rightSum用于记录左右两侧的数组和
int leftSum = Integer.MIN_VALUE, rightSum = Integer.MIN_VALUE;
int sum = 0;
//设置maxLeft和maxRight用于记录子数组左右头元素的位置
int maxLeft = mid, maxRight = mid + 1;
//从左子数组的最右边开始向左遍历,寻找最大子数组
for (int i = mid; i >= low; i--) {
sum += a[i];
if (sum > leftSum) {
leftSum = sum;
maxLeft = i;
}
}
sum = 0;
//从右子数组的最左边开始遍历,寻找最大子数组
for (int j = mid + 1; j <= high; j++) {
sum += a[j];
if (sum > rightSum) {
rightSum = sum;
maxRight = j;
}
}
ChildArray child = new ChildArray ();
child.begin = maxLeft;
child.end = maxRight;
child.sum = leftSum + rightSum;
return child;
}
public static ChildArray findMaximumSubarray (int[] a, int low, int high) {
ChildArray child = new ChildArray ();
ChildArray left = new ChildArray ();
ChildArray right = new ChildArray ();
ChildArray cross = new ChildArray ();
if (high == low) { // 若果数组中仅剩下一个元素,那么该一元素数组就是自身的最大子数组
child.begin = low;
child.end = high;
child.sum = a[low];
return child;
} else { // 若果数组中元素大于1,则继续分解,直至只剩下一个元素为止
//寻找到原数组中点,将原数组等分
int mid = (low + high) / 2;
left = findMaximumSubarray (a, low, mid);
right = findMaximumSubarray (a, mid + 1, high);
cross = findMaxCrossingSubArray (a, low, mid, high);
//对三者进行比较,寻找最大的那个
if (left.sum >= right.sum && left.sum >= cross.sum) {
return left;
} else if (cross.sum >= left.sum && cross.sum >= right.sum) {
return cross;
} else {
return right;
}
}
}
static class ChildArray {
int begin, end;
int sum;
public ChildArray () {}
public ChildArray (int begin, int end, int sum) {
this.begin = begin;
this.end = end;
this.sum = sum;
}
}
}
通过分治,时间复杂度被降低到了O(nlng),总结一下为什么会有这种变化:
分治并不意味着少算一些情况,想要得到最终的答案,必须做到“面面俱到”,任何一种情况的遗失都可能与正确擦肩而过。
我们都知道,分治的原则是:子问题必须和原问题完全一致,且通过子问题的答案可以得到原问题的答案。但是在这个问题中,其实通过子问题的答案并不能得到原问题的答案,正如一开始的例子,但是我们通过一个“补丁”解决了这个问题,即寻找横跨两边的最大子数组。
然而到底为什么时间复杂度能够降低呢?原因在于我们减少了比较的次数,在合并的过程中,一旦被认定不是最大子数组,那么就不再可能与其他数组进行比较。