最大子数组

给定一个数组,其中有正数,也有负数,现在要求寻找其中连续的、和最大的子数组。

很容易想到的,可以暴力求解,把每个子数组都找出来,然后求和,进行比较。但是长度为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),总结一下为什么会有这种变化:

分治并不意味着少算一些情况,想要得到最终的答案,必须做到“面面俱到”,任何一种情况的遗失都可能与正确擦肩而过。

我们都知道,分治的原则是:子问题必须和原问题完全一致,且通过子问题的答案可以得到原问题的答案。但是在这个问题中,其实通过子问题的答案并不能得到原问题的答案,正如一开始的例子,但是我们通过一个“补丁”解决了这个问题,即寻找横跨两边的最大子数组。

然而到底为什么时间复杂度能够降低呢?原因在于我们减少了比较的次数,在合并的过程中,一旦被认定不是最大子数组,那么就不再可能与其他数组进行比较。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值