执行代码参见GitHub
https://github.com/GloryXu/algorithm/tree/master/src/main/java/com/redsun/algorithm/subarraysum
逛论坛无意中发现一个算法帖子,一时间也没想出更好的解决方式,搜索了下,整理下。
暴力破解
@Override
public void execute() {
for (int i = 0; i < arr.length; i++) {
// 遍历从i开始的所有子数组
for (int j = i; j < arr.length; j++) {
int thisSum = 0;
// 累计求和
for (int k = i; k <= j; k++) {
thisSum += arr[k];
}
if (thisSum > maxSum) {
// 记录最大子数组的位置和长度
maxSum = thisSum;
start = i;
end = j;
}
}
}
}
外面两层循环,里面一层循环求和,再进行比较,最后求出一个最大的子数组。
在求出最大子数组同时,记录下对应的start和end位置,即为最大子数组的对应下标。
该种算法的时间复杂度O(n^3)
分治法
代码块1
private int findMaxChildArr(int[] arr, int left, int right) {
if (left == right) {
// 如果左等于右,则说明这个子数组只存在一个元素,则返回它用于比较
return arr[left];
} else {
// 递归思想
int middle = (left + right) / 2;
// 寻找左侧数组最大值;
int leftMax = findMaxChildArr(arr, left, middle);
// 寻找右侧数组最大值;
int rightMax = findMaxChildArr(arr, middle + 1, right);
// 寻找中间最大值
int middleMax = findMiddleMax(arr, left, right, middle);
// 比较求出值
if (leftMax >= rightMax && leftMax >= middleMax) {
return leftMax;
} else if (rightMax >= leftMax && rightMax > middleMax) {
return rightMax;
} else {
return middleMax;
}
}
}
代码块2
public int findMiddleMax(int[] arr, int left, int right, int middle) {
int lmidMax = -1000;
int rmidMax = -1000;
int sum = 0;
// 找到向左的最大前缀
for (int i = middle; i >= left; i--) {
sum += arr[i];
if (sum > lmidMax) {
lmidMax = sum;
}
}
// 找到向右的最大后缀
sum = 0;
for (int i = middle + 1; i <= right; i++) {
sum += arr[i];
if (sum > rmidMax) {
rmidMax = sum;
}
}
return lmidMax + rmidMax;
}
- 将数组一分为二,那么该数组最大的子数组只可能有三种情况,位于左边,位于右边,位于中间(部分左边,部分右边)
- 那么就只要比较左边最大L1,右边最大R1,中间最大M1,得出的结果即是整个数组的最大子数组
- 在求左边最大L1(右边最大R1)的时候又回到了第一点,可再将左边(右边)的数组进行拆分再求对应左边最大L2(右边最大R2),依次递归最终
left=right
- 横跨中间的最大值又是另一种求法,从
middle—>left
和middle—>right
分别求最大,连起来即是最大,详见代码块2。
该算法的时间复杂度为O(N*LogN)
,个人理解:(二分法复杂度LogN)*(middle求最大值的N)
该方法没想到怎么求解出对应最大子数组的下标,有会的童鞋指导下。
贪心算法
@Override
public void execute() {
/**
* sumStart ,sumEnd 分别表示累加数组的start位和end位
* 也相当于 sumStart 是前一次计算结果,sumEnd 是后一次计算结果
* 这里代码中潜在的关系是
* sumEnd = sumStart + arr[end]
*/
int sumStart = 0;
int sumEnd = 0;
int minStart = 0;// 表示最小的 minStart
for (int endIndex = 0; endIndex < arr.length; endIndex++) {
// 相当于构建累加数组 sumEnd 的过程,sumEnd[0..end] = sumEnd[0..end-1] + arr[end]
sumEnd = sumEnd + arr[endIndex];
// 寻找 minStart,minStart[j] = min(minStart[j-1],sum[0..j])
if (sumStart < minStart) {
minStart = sumStart;
/**
* 如果此时sum[0..(endIndex - 1)](即sumStart) < minStart
* 则最大子数组的start只可能从endIndex开始
* 通俗一点就是endIndex(0~endIndex-1)之前的都已经被赋给minStart了
*/
this.start = endIndex;
}
// 更新 sumTotal
if (sumEnd - minStart > maxSum) {
maxSum = sumEnd - minStart;
/**
* 如果maxSum值变动,说明最大子数组的end值为当前endIndex
*/
this.end = endIndex;
}
sumStart = sumStart + arr[endIndex];
}
}
- 因为是连续子数组,所以对于一个数组一定会存在end和start满足图片中的公式
- 所以最终演化成求解minStart和maxSum的两个,即是代码块中的两个判断的目的
该算法也是目前了解到的最优解,核心思想就是将用到了上一次循环的结果,不必每次都重新来过。代码中的注释已经很详细了,可以仔细体会。
只遍历了一次整个数组,时间复杂度O(N)
。
运行结果
最终运行的结果如下