问题描述:
在一组无序的整数(包含负数)中,找出子序列之和在所有子序列中最大的值.若最大子序列之和为负数,则定义最大子序列之和为0.
例子:
一组整数为[1,-3,-2,4,-3,7,-2],则最大子序列为[4,-3,7],故最大和为8.
分析:
1.
最简单的方法,莫过于把所有的子序列都取出来,计算出和,找到最大的子序列;
public class Test {
public static void main(String[] args){
Test t = new Test();
int[] arr = new int[]{1,-3,-2,4,-3,7,-2};
System.out.println(t.findMax(arr));
}
public int findMax(int[] arr){
//若整数个数为0
if(arr == null || arr.length == 0)
return 0;
//记录最大值
int max = 0;
//第一层循环,记录子序列开始位置
for(int i = 0; i < arr.length; i++){
//第二层循环,记录子序列结束位置
for(int j = i; j < arr.length; j++){
//记录子序列之和
int sum = 0;
//第三层循环,计算子序列之和
for(int k = i; k <= j; k++){
sum += arr[k];
}
//判断是否是最大值
max = max >= sum ? max : sum;
}
}
return max;
}
}
该方法有三层for循环,时间复杂度为O(N^3).
2.
分析第一种方法,可知在第三层循环中,重复计算了前面的子序列.
对于一组整数为[1,-3,-2,4,-3,7,-2],在i = 0,j = 1的时候计算了序列[1,-3],那么在[1,-3,-2]的时候,可以直接利用前面的结果而无需重复计算.即对于同一个起点i,可以重复利用前面的计算结果.
public class Test {
public static void main(String[] args){
Test t = new Test();
int[] arr = new int[]{1,-3,-2,4,-3,7,-2};
System.out.println(t.findMax(arr));
}
public int findMax(int[] arr){
//若整数个数为0
if(arr == null || arr.length == 0)
return 0;
//记录最大值
int max = 0;
//第一层循环,记录子序列开始位置
for(int i = 0; i < arr.length; i++){
//记录子序列之和
int sum = 0;
//第二层循环,记录子序列结束位置
for(int j = i; j < arr.length; j++){
//重复利用统一起点i的序列之和
sum += arr[j];
//判断是否是最大值
max = max >= sum ? max : sum;
}
}
return max;
}
}
该方法有二层for循环,时间复杂度为O(N^2).
3.
那我们换一个角度考虑,对于一个整数序列[1,-3,-2,4,-3,7,-2],如果按照分治的思想,将其分为两部分[1,-3,-2,4]和[-3,7,-2],则其最大的子序列无非三种情况,左边序列中的最大值,右边序列中的最大值,或者是以左边序列最右边的整数为头向左的最大子序列之和加上以右边序列最左边的整数为头向右的最大子序列之和的值.
[1,-3,-2,4]最大和为4,[-3,7,-2]最大和为7,([1,-3,-2,4]以4为头的最大和为4)+([-3,7,-2]以-3为头的最大和为4=8),分析正确.
public class Test {
public static void main(String[] args) {
Test t = new Test();
int[] arr = new int[] { 1, -3, -2, 4, -3, 7, -2 };
System.out.println(t.findMax(arr, 0, arr.length - 1));
}
public int findMax(int[] arr, int left, int right) {
// 递归结束条件
if (left >= right) {
if (arr[left] >= 0)
return arr[left];
else
return 0;
}
// 记录中间数的索引,用来分数组
int center = (left + right) / 2;
// 左边序列中的最大值
int maxLeft = findMax(arr, left, center);
// 右边序列中的最大值
int maxRight = findMax(arr, center + 1, right);
//以左边序列最右边的整数为头向左的最大子序列之和
int maxl = 0;
int sum = 0;
for(int i = center; i>= left; i--){
sum += arr[i];
maxl = maxl >= sum ? maxl : sum;
}
//以右边序列最左边的整数为头向右的最大子序列之和
int maxr = 0;
sum = 0;
for(int i = center + 1; i<= right; i++){
sum += arr[i];
maxr = maxr >= sum ? maxr : sum;
}
//最大的和
int max = (maxLeft >= maxRight) ? maxLeft : maxRight;
max = max >= (maxl + maxr) ? max : (maxl + maxr);
return max;
}
}
该方法采用递归的方法,分治处理,时间复杂度为O(NlogN).
4.
继续分析第二种方法,还有没有优化的空间.在第二种方法中,每次起点i变化一次,都要从头进行一次,那么每轮进行的计算并没有充分利用.
设想一下最大子序列i---j,该序列的前缀序列以起点i开头,且前缀序列之和不能为负,因为若前缀为负,则后面的序列之和应该更大.
所以在第二层循环中,若子序列之和为负,则i可以直接移动到j,也就是说,实际上整数序列只需要走一遍.
对于整数[1,-3,-2,4,-3,7,-2],假设i = 0, j = 3,子序列1,-3,-2,4的和为0,故最大的子序列一定不包含该序列,i直接移动到j. 也就是说直接从3开始走,符合上面所说,只走一遍.
public class Test {
public static void main(String[] args){
Test t = new Test();
int[] arr = new int[]{1,-3,-2,4,-3,7,-2};
System.out.println(t.findMax(arr));
}
public int findMax(int[] arr){
//若整数个数为0
if(arr == null || arr.length == 0)
return 0;
//记录最大值
int max = 0;
//记录前缀序列之和
int sum = 0;
//i记录子序列开始位置
for(int i = 0; i < arr.length; i++){
sum += arr[i];
max = max > sum ? max : sum;
if(sum <= 0)
sum = 0;
}
return max;
}
}
该方法的时间复杂度只有O(N),效率提升明显.