题目要求:
- 输入一个整形数组,数组里有正数也有负数。
- 数组中连续的一个或多个整数组成一个子数组,每个子数组都有一个和。
- 求所有子数组的和的最大值。要求时间复杂度为O(n)
- 发表一篇博客文章讲述设计思想,出现的问题,可能的解决方案(多选)、源代码、结果截图、总结。
设计思想及代码和结果截图:
1.最开始的想法很简单,使用穷举法。我们列出所有的子数组之和,然后取出其中的最大值,代码如下:
public static void main(String[] args) {
int [] a = {1,-2,3,10,-4,7,2,-5};
int maxsofar = 0;
int sum = 0;
//进入循环
for(int i=0;i<8;i++) {
sum=0;
//每次求出一个子数组的和就和目前的最大值进行比较
for(int j=i;j<8;j++) {
sum+=a[j];
maxsofar = Math.max(maxsofar, sum);
}
}
System.out.println(maxsofar);
}
运行结果如图:
可以看到,结果是正确的,但这个的时间复杂度为O(n2),明显超出了我们题目要求的O(n),通常最直观的算法是最慢的,我们接着使用别的算法。
2.之后,意识到老师提出的动态规划算法,于是在网上查阅了相关资料,发现了Kadane算法。这个算法是动态规划的一种具体应用,其设计思想也很好理解:对于数组a,用ci标记子数组a[0..i]的最大和,那么则有 ci=max{ai,ci−1+ai} 但仔细观察我们发现,若ci-1<=0 那么ci=ai。用e表示以当前为结束的子数组的最大和,以替代数组c,那么我们可以表示为:e=max{ai,e+ai}。代码实现如下:
public static void main(String[] args) {
int [ ] a = {1,-2,3,10,-4,7,2,-5};
int result = FindGreatestSumOfSubArray(a);
System.out.println(result);
}
private static int FindGreatestSumOfSubArray(int[] a) {
int len=a.length;
if(len==0) {
return 0;
}
//第一个变量对应公式中的e
//第二个变量记录已扫描到的子数组最大值的和
int max_ending_here,max_so_for;
max_ending_here=max_so_for=a[0];
//对数组进行扫描,扫描一次记录一次
for(int x:a) {
max_ending_here = Math.max(x, max_ending_here+x);
max_so_for = Math.max(max_so_for, max_ending_here);
}
return max_so_for;
}
结果截图如下:
可以看到,Kadane算法只循环了数组一次,所以时间复杂度为O(n),很好的满足了题目的要求。
之后,老师提出了进阶的问题,如果将数组变成一个循环数组(环形数组),即头尾相接,又该如何求解最大子数组的和呢?这次的问题不再有时间复杂度限制。
我们注意到,可以把问题分成两个部分:如果最大子数组的和在数组的下标范围内,那么就可以直接应用上面的Kadane算法,可如果最大子数组的和超出了下标范围,又该如何求解呢?我查阅了相关资料,在leetnode发现了一位大神提出的思路:跨越边界的情况可以对数组求和再减去无环的子数组的最小和,即可得到跨越边界情况下的子数组最大和。将两种情况的最大值取出,即是问题的解。下面是代码:
public static int maxSubarraySumCircular(int[] A) {
if(A==null||A.length<1){
return 0;
}
int curMax,max,curMin,min,sum;
curMax = max = curMin =min = sum = A[0];
for(int i=1;i<A.length;i++){
sum+=A[i];
curMax = Math.max(A[i],curMax+A[i]);
curMin = Math.min(A[i],curMin+A[i]);
max = Math.max(max,curMax);
min = Math.min(min,curMin);
}
if(max<0)
return max;
return Math.max(sum-min,max);
}
public static void main(String[] args) {
int i = 0, n;
int a[] = new int[100];
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
for (i = 0; i < n; i++) {
a[i] = scanner.nextInt();
}
int max = maxSubarraySumCircular(a);
scanner.close();
System.out.println(max);
}
运行结果截图:
可以看到,结果与在leetnode中的结果一致 说明了该算法的正确性。但为什么用数组的和减去无环数组中子数组的最小和就是结果,我还没有思考出来,暂且留个悬念。
总结:通过这次算法的编写和思考,可以清楚的看到,通常看到题目冒出的算法不是最简单的。我们可以使用数学上的各种知识来对算法进行优化。一个数量级是很大的概念,在真正的工程中算法优化了一个数量级意味着软件的运行速度可以快好多倍。以后在写软件时一定要尽量使用快且稳的算法,做到稳定又快。