所谓最大子数组就是连续的若干数组元素,如果其和是最大的,那么这个子数组就称为该数组的最大子数组。最大子数组是很多问题的抽象,比如购买股票。如果把相邻两天的股价之差作为数组元素,那么求在连续的某个时间段内买入股票的最佳时间和卖出股票的最佳时间就可以抽象为计算最大子数组的问题。下面分别介绍两种算法求解最大子数组问题,算法思想均来自算法导论这本程序员的圣经。
分治法
分治法实现思路比较简单。如果把一个数组从中间点分为左右两个子数组,那么最大子数组可能存在的位置可能存在的情况有三种:
- 最大子数组在左边的数组中
- 最大子数组在右边数组中
- 最大子数组跨越了左右两个子数组
左右两个子数组可以以同样的方式递归的划分为规模更小的子数组,直到不能划分为止,此时出现了递归的基本情况。比较三种情况的计算结果就能得知最大子数组。分治法可以在O(NlogN)的时间复杂度内计算出最大子数组,效率还是比较乐观的。
在第三种情况中采用的算法是,从中间点向数组的左右两边遍历,分别求出左右两边的最大子数组。然后左右两边的子数组相加即为跨越中间点的最大子数组。本人研究这中算法有一点心的:寻找最大子数组的过程其实本质上是findMaximumSubArray方法在比较计算,该方法充当了分治法三个步骤中分解、解决、合并的合并步骤。合并的方法往往是真正做事情的方法,而解决的步骤都会包含合并步骤。而解决又不断递归的调用自身,从而以一种大问题化为小问题,小问题最后又合并为大问题的方式把问题解决。不得不说分治法是一种非常强大的思想!不想当一辈子码农的码农们真应该静下心来好好学习学习,而不是整天担忧程序员三十岁以后该怎么怎么办。做好今天的自己,明天自然是美好的!这话说给自己,同时也和所有同行共勉。
以下为java的实现和测试。
/**
* desc : 分治法求解最大子数组
* Created by tiantian on 2018/7/18.
*/
public class FindMaximumSubArray {
public static Map findMaximumSubArray(Integer[] array, int low, int height) {
Map<String, Integer> map = new HashMap();
if (low == height) {
map.put("low", low);
map.put("height", height);
map.put("max",array[low]);
return map;
}
int mid = (low+height)/2;//分解
Map<String, Integer> left = findMaximumSubArray(array, low, mid);//解决
Map<String, Integer> right = findMaximumSubArray(array, mid+1, height);// 解决
Map<String, Integer> midMap = findMaxCrossingSubArray(array, low, mid, height);//合并
Map retMap = new HashMap();
if (left.get("max") >= right.get("max") && left.get("max") >= midMap.get("max")) {
retMap.put("low", left.get("low"));
retMap.put("height", left.get("height"));
retMap.put("max", left.get("max"));
} else if (right.get("max") >= left.get("max") && right.get("max") >= midMap.get("max")) {
retMap.put("low", right.get("low"));
retMap.put("height", right.get("height"));
retMap.put("max", right.get("max"));
} else {
retMap.put("low", midMap.get("low"));
retMap.put("height", midMap.get("height"));
retMap.put("max", midMap.get("max"));
}
return retMap;
}
/**
* desc:求横穿中间点的最大子数组
*/
public static Map findMaxCrossingSubArray(Integer[] array, int low, int mid, int height) {
int leftSum = -65535;
int maxLeft = 0;
int tempSum = 0;
for (int i = mid; i >= low; i--) {
tempSum = tempSum + array[i];
if (tempSum > leftSum) {
leftSum = tempSum;
maxLeft = i;
}
}
tempSum = 0;
int rightSum = -65535;
int maxRight = 0;
for (int j = mid+1; j <= height; j++) {
tempSum = tempSum + array[j];
if (tempSum > rightSum) {
rightSum = tempSum;
maxRight = j;
}
}
Map<String, Integer> map = new HashMap();
map.put("low", maxLeft);
map.put("height", maxRight);
map.put("max", leftSum+rightSum);
return map;
}
}
测试:
/**
* desc : 最大子数组测试客户端
* Created by tiantian on 2018/7/19
*/
public class TestClient {
public static void main(String[] args) {
Integer[] testArray = {-500,10,5,5,2,-3,-29,-10,-50,22,-23,10,150,1,9,-900,-26,3,99,7};
for (int i = 0; i < testArray.length; i++) {
System.out.print(testArray[i] + ",");
}
int length = testArray.length;
Map map = FindMaximumSubArray.findMaximumSubArray(testArray, 0, length-1);
System.out.println("左下标:" + map.get("low"));
System.out.println("右下标:" + map.get("height"));
System.out.println("最大和:" + map.get("max"));
}
}
// 输出:
// -500,10,5,5,2,-3,-29,-10,-50,22,-23,10,150,1,9,-900,-26,3,99,7
// 左下标:11
// 右下标:14
// 最大和:170
线性法
实在不知道如何命名这种算法,因为该算法能在线性时间内计算出最大子数组,所以姑且叫做线性法吧。整个计算过程只需要遍历一遍数组就能得出结论,很明显,这是一种性能优于分治法的方式。以下为实现代码,代码中解释了算法的思路。
/**
* desc : 线性时间查找最大子数组,返回两个下标
* Created by tiantian on 2018/7/19
*/
public class MaximumSubArray {
public static Integer find(Integer[] array) {
// 当前迭代元素之前的最大子数组之和(不包含当前迭代元素)
Integer ahead = array[0];
// 包含当前迭代元素的最大子数组
Integer containCurent = array[0];
/**
* 当前最大子数组有两种种情况:
* 情况1:不包含当前迭代元素,也就是 array[i].
* 情况2:包含当前迭代元素,要么最大子数组就是当前迭代元素array[i];
* 要么最大子数组在在array[0...i]中,在array[0...i]中的话必然和前一个元素相连,
* 那么,当前元素加上前一个元素包含自身的最大子数组即为当前元素包含自身的最大子数组.
*/
for (int i = 0; i < array.length; i++) {
if ((containCurent + array[i]) >= array[i]) {
containCurent = containCurent + array[i];
} else {
containCurent = array[i];
}
if (containCurent > ahead) {
ahead = containCurent;
}
}
return ahead;
}
public static void main(String[] args) {
Integer[] testArray = {-500,10,5,5,2,-3,-29,-10,-50,22,-23,10,150,1,9,-900,-26,3,99,7};
for (int i = 0; i < testArray.length; i++) {
System.out.print(testArray[i] + ",");
}
System.out.println("");
Integer max = find(testArray);
System.out.println(max);
}
}
// 输出:
// -500,10,5,5,2,-3,-29,-10,-50,22,-23,10,150,1,9,-900,-26,3,99,7,
// 170
两种方式的测试数据相同,结果也相同。感兴趣的伙伴可以自己验证下。当然严格的算法正确性这里就不多解释了,因为就算给出一万组测试也不能证明,还是要用循环不变式等科学的方式严谨的证明。
至此两种方式已介绍完毕,感兴趣的朋友可以关注我。本人博客每周都会更新新的内容,目前连续更新的有设计模式,算法。以后还会写JVM、并发等方面的。