题目描述:连续子数组的最大和(连续最大公共子序列) 或许其实只有4个解法(看最后总结)
思路1:复杂度最高也是最直观的,求得所有的子序列,进行比较。时间复杂度O(n^3)
思路2:对第一个算法进行改进。有些重复计算的值,避免再次计算,因此时间复杂度O(n^2)
思路3:使用分治的思想,同时也使用了递归。时间复杂度O(n*logn)
思路4:动态规划,时间复杂度O(n)
思路5:叫不出名字的解法,有的称扫描法?而且时间复杂度也仅为O(n)
思路1实现代码:
public class Solution {
public int FindGreatestSumOfSubArray(int[] array) {
int maxSum = array[0];//如果设为0,在某些负数数组中会出错
int sum;
//最外层循环,每一层循环,都能拿到以当前角标i为数组的第一个元素时,所有子序列的最大值
//如{1,-1,4,8}就分成{1,-1,4,8} {-1,4,8} {4,8} {8}四个部分分别求
for (int i = 0; i < array.length; i++) {
//第二层循环,以i=1为例,{-1,4,8} 由于j的递增,又分成了{-1,4,8}{4,8}{8} 三组
for (int j = i; j < array.length; j++) {
sum = 0;
//第三次循环,以i=1为例,j=1时,k的变化得到{-1} 结果 -1
//第三次循环,以i=1为例,j=2时,k的变化得到{-1,4} 结果3 -1+4
//第三次循环,以i=1为例,j=3时,k的变化得到{-1,4,8} 结果11 利用循环求和-1+4+8
for (int k = i; k <= j; k++) {
sum += array[k];// 计算a[i]到a[j]的和
}
if (sum > maxSum) {
maxSum = sum;
}
}
}
return maxSum;
}
}思路2实现:可以看到,{1,-1,4,8}在第以i=1为例,j=3时,第三轮得到{-1,4,8}的和的过程是通过一轮循环 -1+4+8得到,但是k变化的这一轮循环是完全可以避免的,可以用{-1,4}的结果加上8这样的方式得到。因为{-1,4}在j=2的时候已经计算过了,不需要再次循环从-1+4+8来一次循环,而是用之前计过的算结果就好了,因此可以降低复杂度,代码实现如下
public class Solution {
public int FindGreatestSumOfSubArray(int[] array) {
int maxSum = array[0];
int sum;
//最外层循环,如{1,-1,4,8}就分成{1,-1,4,8} {-1,4,8} {4,8} {8}四个部分分别求
for (int i = 0; i < array.length; i++) {
sum = 0;
//以i=1为例 {-1,4,8} 在完成整个第二轮循环中,计算的结果是-1,3(-1+4),11(3+8)
//第二种方法计算11的过程是没有用循环的,而是用上一个的结果3加8得到本次结果。
//第一种方法得到11是-1+4+8,需要循环计算
for (int j = i; j < array.length; j++) {
sum += array[j];
if (sum > maxSum) {
maxSum = sum;
}
}
}
return maxSum;
}
}思路3:分治--把问题分成两个大致相等的子问题,然后递归地对它们求解,这是“分”的部分。“治”阶段将两个子问题的解修补到一起,并可能再做些少量的附加工作,最后得到整个问题的解。
将数组分成两半后,问题的结果可能出现在左边,可能在右边,还可能左边有一部分元素,右边也有一点元素。
例如:D通过递归调用左B和右C,B告诉左最小,c告诉右最小,那么D本身还需要补一个跨中间的比较,然后三个值比较后return,返回的是D整体的最大值。在B中,通过递归调用E,F时,也会运行一个跨中间的比较,然后三值比较,进行return(那么D获得B整个部分的最大),在C通过递归调用G,H时,也会运行一个跨中间的比较,然后三值比较,进行return(那么D获得了C整个部分的最大)
本份代码调整了maxLeftBorderSum和maxRightBorderSum的赋初值的情况,可以进行所有元素是负数的情形,网上看了很多示例,都不能解决所有元素是负数的情况。另外边界条件也是分治(递归)值得注意的。
public class Solution {
public int FindGreatestSumOfSubArray(int[] array) {
return way(array,0,array.length-1);
}
public static int way(int [] arr,int left,int right) {
//这是递归的返回情况,左右边界相等,直接返回
if (left == right) {
return arr[left];
}
//分割序列 分成两半
int center = (left + right) / 2;
//递归处理子序列 注意,递归返回的是子序列的最大值
int maxLeftSum = way(arr, left, center);
int maxRightSum = way(arr, center+1, right);
//处理左序列 从中间边界向左边界计算
//上面两行拿到了左和右两个部分最大值,现在处理合起来的部分
int maxLeftBorderSum = arr[center]; //左边界中,求和过程中的,带着center点的最大值
int leftBorderSum = 0; //左边界求和的值
//通过本轮循环,可以达到当前左边部分的最大值
for (int i = center; i >= left; i--) {
leftBorderSum += arr[i];
if (leftBorderSum > maxLeftBorderSum) {
maxLeftBorderSum = leftBorderSum;
}
}
//处理右序列 从中间边界向后边界计算
int maxRightBorderSum = arr[center+1];//右边界中,带着center+1点的最大值
int rightBorderSum = 0; //这是右边界所有的和
for (int i = center+1; i <= right; i++) {
rightBorderSum += arr[i];
if (rightBorderSum > maxRightBorderSum) {
maxRightBorderSum = rightBorderSum;
}
}
//返回最大子序列和 maxLeftBorderSum+maxRightBorderSum是包含center中点的情况
//所以是三个数,包含中间,左最大,右最大
return Max(maxLeftBorderSum+maxRightBorderSum,maxLeftSum,maxRightSum);
}
//三元运算求最值
public static int Max(int a,int b, int c) {
return a > b? (a > c ? a : c ): (b > c ? b : c);
}
}第四种 动态规划(摘过来的答案,以后再来理解)
1、状态方程 : max(dp[i]) = getMax(max(dp[i-1]) + arr[i] ,arr[i])
2、上面式子的意义是:我们从头开始遍历数组,遍历到数组元素 arr[i] 时,连续的最大的和可能为
max(dp[i-1])+arr[i],也可能为arr[i] ,做比较即可得出哪个更大,取最大值。时间复杂度为O(n)。
public class Solution {
public int FindGreatestSumOfSubArray(int[] array) {
if(array.length == 0) return 0;
int sum = array[0];
int temp = array[0]; //注意初始值 不能设为0 防止只有负数
for(int i = 1; i < array.length; i++) //从1开始 因为0的情况在初始化时完成了
{
temp = (temp < 0) ? array[i] : temp + array[i];
sum = (temp > sum) ? temp : sum;
}
return sum;
}
}思路5:
我们可以从第一个数开始算起,每往后加一个数便更新一次和的最大值;当和成为负数时,则表明此前序列无法为后面提供最大子序列和,(不管后面是否正负,你这里为负数,加上你这个负数就更小了,还不如不加)因此必须重新确定序列首项,虽然重新确定序列首部,但是我已经用一个变量记录了之前的最大值了。
极端情况:全是负数时,temp变量一直是等于某个单个的元素,在移动,并与max比较,因此max通过比较也等于的是最小的负数
public class Solution {
public int FindGreatestSumOfSubArray(int[] array) {
int maxSum = array[0]; //设置为0,会导致有全负的数组的结果为0的错误
int temp = 0;
for( int i = 0; i < array.length ; i ++ ){
temp += array[ i ];
if(temp > maxSum)
maxSum = temp;
else if(temp < 0)
temp = 0;
}
return maxSum;
}
}发现上下这两种解法其实是一个意思
public class Solution {
public int FindGreatestSumOfSubArray(int[] array) {
int temp=array[0];
int maxSum=array[0];
for(int i=1;i<array.length;i++){
if(temp<0)
temp=array[i];
else
temp+=array[i];
if(temp>maxSum)
maxSum=temp;
}
return maxSum;
}
}
总结,虽然动态规划的原理我现在暂时不懂,但是从代码上看,貌似摘过来的动态规划代码所描述的过程,和第五种思路的两种解法都非常相似。
无非就是(临时求和变量temp )temp小于0时,则temp应该重新赋值,而上面的写法有些是赋值0,有些是等于下个变量,其实是一个意思,因为你下一步总是要加下一个变量A,即temp=A,或者是temp=0再temp+=A,这两个写法上面都有,没区别。而如果temp大于0,就让temp加上下一个变量A,也就是temp=temp+A。再判断temp和当前记录的最大值maxsum,更新最大值。然后进入下一轮判断。三份代码的流程都是这样的,可能思路不同,但是写出代码的执行过程却非常相似,菜鸟表示很疑惑。
总之:由于复杂度是O(n),意味着每个元素只扫描了一次,需要保证扫描一次后,当前保留的maxsum是扫描过的最大值,所以意味着,最大子序列的头部在移动,并不是一直保持索引为0。所以扫描过程中需要跳过一些负面元素的影响,而temp就起到了这个所用。



京公网安备 11010502036488号