Algorithm —— 最大子数组求解(五)

Algorithm —— 最大子数组求解


《算法导论》中引出了一个求某个数组的和最大子数组问题:在原数组A中,求一个子数组a,它的各元素值的和是A的各个子数组中最大的;且子数组a的各元素下标值要连续。

要注意的是,如果要求某个数组的最大子数组,则此数组中的值必须要包含负值;否则,求最大子数组是没有意义的,因为此时,整个数组的和肯定是最大的,就不必再求了。

我们使用分治策略来解决该问题。假定我们要寻找数组A[low...high]的最大子数组,因为要使用分治技术,这也就意味着我们要将数组A划分为两个规模尽量相等的子数组。也就是找到数组A的中央位置mid,然后考虑求解两个子数组A[low...mid]和A[mid+1...high]。

我们可以知道,数组A[low...high]的任何连续子数组A[i...j]所处的位置必然是一下三种情况之一:

  1. 完全位于子数组A[low...mid]中,因此low =< i =< j =< mid;子数组完全位于A[low...mid]中。
  2. 完全位于子数组A[mid+1...high]中,因此mid < i =<j =< high;子数组完全位于A[mid+1...high]中。
  3. 跨越了中点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]

求数组的最大子数组问题就解决了。













猜你喜欢

转载自blog.csdn.net/csdn_of_coder/article/details/79953729