Algorithm —— 最大子数组求解
《算法导论》中引出了一个求某个数组的和最大子数组问题:在原数组A中,求一个子数组a,它的各元素值的和是A的各个子数组中最大的;且子数组a的各元素下标值要连续。
要注意的是,如果要求某个数组的最大子数组,则此数组中的值必须要包含负值;否则,求最大子数组是没有意义的,因为此时,整个数组的和肯定是最大的,就不必再求了。
我们使用分治策略来解决该问题。假定我们要寻找数组A[low...high]的最大子数组,因为要使用分治技术,这也就意味着我们要将数组A划分为两个规模尽量相等的子数组。也就是找到数组A的中央位置mid,然后考虑求解两个子数组A[low...mid]和A[mid+1...high]。
我们可以知道,数组A[low...high]的任何连续子数组A[i...j]所处的位置必然是一下三种情况之一:
- 完全位于子数组A[low...mid]中,因此low =< i =< j =< mid;子数组完全位于A[low...mid]中。
- 完全位于子数组A[mid+1...high]中,因此mid < i =<j =< high;子数组完全位于A[mid+1...high]中。
- 跨越了中点mid,因此lwo =< i =< mid < j =< high;子数组跨越了中点mid。
因此,A[low...high]的一个最大子数组所处的位置必然是上述三种情况之一。对于前两种情况,我们可以递归求解,因为 这两个问题仍是最大子数组问题,只是规模更小;剩下的工作就是寻找跨越中点的最大子数组。所以问题的最终结果就是在三种情况中选取和最大者。
寻找跨越中点的最大子数组问题并不是原问题规模更小的实例,因为它加入了限制——求出的子数组必须跨越中点。任何跨越中点的子数组都由两个子数组A[i...mid]和A[mid+1...j]组成,其中low =< i =< mid < j =< high。因此,我们只需找出形如A[i...mid]和A[mid+1...j]的最大子数组,然后将其合并即可。此问题的函数如下所示:
/**
*
* @author coder
*
* 代表'和最大子数组'信息的封装类型
*
*/
private class MaxSumSubArray {
public MaxSumSubArray(int left_index, int right_index, int sum) {
this.left_index = left_index;
this.right_index = right_index;
this.sum = sum;
}
int left_index;// 数组的左下标边界值
int right_index;// 数组的右下标边界值
int sum;// 此子数组各元素的和值
@Override
public String toString() {
return "MaxSumSubArray [left_index=" + left_index + ", right_index=" + right_index + ", sum=" + sum + "]";
}
}
/**
* 返回一个跨越中点的'和最大子数组'的边界,及此数组中所有值的和;因为要寻找的'和最大子数组'下标必须要跨越中点,所以该子数组的下标范围一定是从中点向左右两边延伸、且连续;
*
* 对于左数组,我们只需在下标范围[low...mid]中,从mid值开始由右向左依次计算各元素相加的和,并逐一比较各次所得和的大小;记录当前得到的最大的和与此时运算的数组下标值i;循环结束后,就得到了这个和最大子数组的左半部分a[i...mid]
* 对于右数组,也是类似,从(mid+1)开始,从左向右遍历计算各元素相加的和;得到当前最大的和值时的下标j;循环结束后,就得到这个子数组的右半部分b[mid+1...high]
*
* 合并子数组a/b,就得到了当前跨越中点的'和最大子数组'c[i...j];
*
* @param array
* 当前需要处理的数组
* @param low
* 在当前数组中需要处理的子数组的下标左边界
* @param mid
* 在当前数子组中需要处理的下标范围对应的下标中点
* @param heigh
* 在当前数组中需要处理的子数组的下标右边界
* @return 返回一个MaxSumSubArray实例,即 跨越中点的'和最大子数组'的封装对象
*/
private MaxSumSubArray findMaxCrossingSubArray(int[] array, int low, int mid, int high) {
int left_sum = 0;// 记录每次左边找到的最大和值
int sum = 0; // 记录左边子数组所有元素的和值
int max_left = -1;// 记录每次找到左边子数组最大和值时的下标值
for (int i = mid; i >= low; i--) {
sum = sum + array[i];
if (sum > left_sum) {
left_sum = sum;
max_left = i;
}
}
int right_sum = 0;// 记录每次右边找到的最大和值
sum = 0; 记录右边子数组所有元素的和值
int max_right = -1;// 记录每次找到右边子数组最大和值时的下标值
for (int j = mid + 1; j <= high; j++) {
sum = sum + array[j];
if (sum > right_sum) {
right_sum = sum;
max_right = j;
}
}
MaxSumSubArray obj = new MaxSumSubArray(max_left, max_right, left_sum + right_sum);
return obj;
}
此时,我们就解出了求跨越中点的最大子数组问题;又由于前两种情况是原问题规模更小的实例,可用递归求解;所以此时求数组A[low...high]的最大子数组问题的解如下:
/**
*
* @author coder
*
* 代表'和最大子数组'信息的封装类型
*
*/
private class MaxSumSubArray {
public MaxSumSubArray(int left_index, int right_index, int sum) {
this.left_index = left_index;
this.right_index = right_index;
this.sum = sum;
}
int left_index;// 数组的左下标边界值
int right_index;// 数组的右下标边界值
int sum;// 此子数组各元素的和值
@Override
public String toString() {
return "MaxSumSubArray [left_index=" + left_index + ", right_index=" + right_index + ", sum=" + sum + "]";
}
}
/**
* 找到目标数组array的一个'和最大子数组',并返回该'和最大子数组'的下标及所有元素和信息
*
* @param array
* 目标源数组
* @param low
* 需要操作的子数组的下标左边界
* @param high
* 需要操作的子数组的下标右边界
* @return 返回从目标值数组中指定的子数组c[low...high]的'和最大子数组'信息
*/
public MaxSumSubArray findMaxImumSubArray(int[] array, int low, int high) {
if (low > high || array == null) {
throw new IllegalArgumentException("findMaxImumSubArray Argument is illegal!");
}
if (high == low) {
return new MaxSumSubArray(low, high, array[low]);
}
int mid = (low + high) / 2;
MaxSumSubArray leftMaxSSArray = findMaxImumSubArray(array, low, mid);
MaxSumSubArray rightMaxSSArray = findMaxImumSubArray(array, mid + 1, high);
MaxSumSubArray crossMidMaxSSArray = findMaxCrossingSubArray(array, low, mid, high);
if (leftMaxSSArray.sum >= rightMaxSSArray.sum && leftMaxSSArray.sum >= crossMidMaxSSArray.sum)
return leftMaxSSArray;
else if (rightMaxSSArray.sum >= leftMaxSSArray.sum && rightMaxSSArray.sum >= crossMidMaxSSArray.sum)
return rightMaxSSArray;
return crossMidMaxSSArray;
}
/**
* 返回一个跨越中点的'和最大子数组'的边界,及此数组中所有值的和;因为要寻找的'和最大子数组'下标必须要跨越中点,所以该子数组的下标范围一定是从中点向左右两边延伸、且连续;
*
* 对于左数组,我们只需在下标范围[low...mid]中,从mid值开始由右向左依次计算各元素相加的和,并逐一比较各次所得和的大小;记录当前得到的最大的和与此时运算的数组下标值i;循环结束后,就得到了这个和最大子数组的左半部分a[i...mid]
* 对于右数组,也是类似,从(mid+1)开始,从左向右遍历计算各元素相加的和;得到当前最大的和值时的下标j;循环结束后,就得到这个子数组的右半部分b[mid+1...high]
*
* 合并子数组a/b,就得到了当前跨越中点的'和最大子数组'c[i...j];
*
* @param array
* 当前需要处理的数组
* @param low
* 在当前数组中需要处理的子数组的下标左边界
* @param mid
* 在当前数子组中需要处理的下标范围对应的下标中点
* @param heigh
* 在当前数组中需要处理的子数组的下标右边界
* @return 返回一个MaxSumSubArray实例,即 跨越中点的'和最大子数组'的封装对象
*/
private MaxSumSubArray findMaxCrossingSubArray(int[] array, int low, int mid, int high) {
int left_sum = 0;// 记录每次左边找到的最大和值
int sum = 0; // 记录左边子数组所有元素的和值
int max_left = -1;// 记录每次找到左边子数组最大和值时的下标值
for (int i = mid; i >= low; i--) {
sum = sum + array[i];
if (sum > left_sum) {
left_sum = sum;
max_left = i;
}
}
int right_sum = 0;// 记录每次右边找到的最大和值
sum = 0; 记录右边子数组所有元素的和值
int max_right = -1;// 记录每次找到右边子数组最大和值时的下标值
for (int j = mid + 1; j <= high; j++) {
sum = sum + array[j];
if (sum > right_sum) {
right_sum = sum;
max_right = j;
}
}
MaxSumSubArray obj = new MaxSumSubArray(max_left, max_right, left_sum + right_sum);
return obj;
}
完整的测试代码如下:
public class AlgorithmTest {
public static void main(String[] args) {
int[] array = { 13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7 };
AlgorithmTest algorithm = new AlgorithmTest();
MaxSumSubArray res = algorithm.findMaxImumSubArray(array, 0, array.length - 1);
System.out.println(res);
}
/**
*
* @author coder
*
* 代表'和最大子数组'信息的封装类型
*
*/
private class MaxSumSubArray {
public MaxSumSubArray(int left_index, int right_index, int sum) {
this.left_index = left_index;
this.right_index = right_index;
this.sum = sum;
}
int left_index;// 数组的左下标边界值
int right_index;// 数组的右下标边界值
int sum;// 此子数组各元素的和值
@Override
public String toString() {
return "MaxSumSubArray [left_index=" + left_index + ", right_index=" + right_index + ", sum=" + sum + "]";
}
}
/**
* 找到目标数组array的一个'和最大子数组',并返回该'和最大子数组'的下标及所有元素和信息
*
* @param array
* 目标源数组
* @param low
* 需要操作的子数组的下标左边界
* @param high
* 需要操作的子数组的下标右边界
* @return 返回从目标值数组中指定的子数组c[low...high]的'和最大子数组'信息
*/
public MaxSumSubArray findMaxImumSubArray(int[] array, int low, int high) {
if (low > high || array == null) {
throw new IllegalArgumentException("findMaxImumSubArray Argument is illegal!");
}
if (high == low) {
return new MaxSumSubArray(low, high, array[low]);
}
int mid = (low + high) / 2;
MaxSumSubArray leftMaxSSArray = findMaxImumSubArray(array, low, mid);
MaxSumSubArray rightMaxSSArray = findMaxImumSubArray(array, mid + 1, high);
MaxSumSubArray crossMidMaxSSArray = findMaxCrossingSubArray(array, low, mid, high);
if (leftMaxSSArray.sum >= rightMaxSSArray.sum && leftMaxSSArray.sum >= crossMidMaxSSArray.sum)
return leftMaxSSArray;
else if (rightMaxSSArray.sum >= leftMaxSSArray.sum && rightMaxSSArray.sum >= crossMidMaxSSArray.sum)
return rightMaxSSArray;
return crossMidMaxSSArray;
}
/**
* 返回一个跨越中点的'和最大子数组'的边界,及此数组中所有值的和;因为要寻找的'和最大子数组'下标必须要跨越中点,所以该子数组的下标范围一定是从中点向左右两边延伸、且连续;
*
* 对于左数组,我们只需在下标范围[low...mid]中,从mid值开始由右向左依次计算各元素相加的和,并逐一比较各次所得和的大小;记录当前得到的最大的和与此时运算的数组下标值i;循环结束后,就得到了这个和最大子数组的左半部分a[i...mid]
* 对于右数组,也是类似,从(mid+1)开始,从左向右遍历计算各元素相加的和;得到当前最大的和值时的下标j;循环结束后,就得到这个子数组的右半部分b[mid+1...high]
*
* 合并子数组a/b,就得到了当前跨越中点的'和最大子数组'c[i...j];
*
* @param array
* 当前需要处理的数组
* @param low
* 在当前数组中需要处理的子数组的下标左边界
* @param mid
* 在当前数子组中需要处理的下标范围对应的下标中点
* @param heigh
* 在当前数组中需要处理的子数组的下标右边界
* @return 返回一个MaxSumSubArray实例,即 跨越中点的'和最大子数组'的封装对象
*/
private MaxSumSubArray findMaxCrossingSubArray(int[] array, int low, int mid, int high) {
int left_sum = 0;// 记录每次左边找到的最大和值
int sum = 0; // 记录左边子数组所有元素的和值
int max_left = -1;// 记录每次找到左边子数组最大和值时的下标值
for (int i = mid; i >= low; i--) {
sum = sum + array[i];
if (sum > left_sum) {
left_sum = sum;
max_left = i;
}
}
int right_sum = 0;// 记录每次右边找到的最大和值
sum = 0; 记录右边子数组所有元素的和值
int max_right = -1;// 记录每次找到右边子数组最大和值时的下标值
for (int j = mid + 1; j <= high; j++) {
sum = sum + array[j];
if (sum > right_sum) {
right_sum = sum;
max_right = j;
}
}
MaxSumSubArray obj = new MaxSumSubArray(max_left, max_right, left_sum + right_sum);
return obj;
}
public void arrayPrint(int[] array) {
System.out.print("[");
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ", ");
}
System.out.println("]");
}
}
输出如下:
MaxSumSubArray [left_index=7, right_index=10, sum=43]
求数组的最大子数组问题就解决了。