welcome to my blog
剑指offer面试题42(java版):连续子数组的最大和
题目描述
HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1)
思路
- 用sum记录连续n项的和, 当sum小于等于0时, 将sum重置为第n+1项的值, 继续向下遍历
- 核心: 当sum小于等于0时要重置sum; 等于0是为了处理起始项, 最开始sum=0, 将起始项array[0]赋值给sum
/*
动态规划:
令f(i)表示索引从0到i这段区间中的和的最大值, 对应代码中的currMax
递推关系
f(i) = f(i-1) + array[i] if f(i-1)>0
f(i) = array[i] if f(i-1)<=0 or i==0
*/
从递推公式开始
class Solution {
public int maxSubArray(int[] nums) {
int n = nums.length;
//dp[i]nums[0,i]上连续子数组的最大和
//dp[i] = dp[i-1]+nums[i]>nums[i]? dp[i-1]+nums[i] : nums[i]
//化简后: dp[i] = dp[i-1]>0? dp[i-1]+nums[i] : nums[i]
int[] dp = new int[n];
dp[0] = nums[0];
int max = nums[0];
for(int i=1; i<n; i++){
if(dp[i-1] > 0){
dp[i] = dp[i-1] + nums[i];
}else{
dp[i] = nums[i];
}
max = Math.max(max, dp[i]);
}
return max;
}
}
//状态压缩
class Solution {
public int maxSubArray(int[] nums) {
int n = nums.length;
int max = nums[0];
int curSum = nums[0];
for(int i=1; i<n; i++){
if(curSum>0){
curSum = curSum + nums[i];
}else{
curSum = nums[i];
}
max = Math.max(max, curSum);
}
return max;
}
}
第三次做; for循环中的顺序:更新sum, 更新max, 有必要的话第二次更新sum
/*
连续子数组
维护子数组的左右边界
*/
public class Solution {
public int FindGreatestSumOfSubArray(int[] array) {
int max = Integer.MIN_VALUE;
int sum=0;
for(int i=0; i<array.length; i++){
//update
sum += array[i];
max = Math.max(max, sum);
//update
if(sum <=0)
sum=0;
}
return max;
}
}
第二遍做,不考虑这种情况:sum+arr[i]会减小,但是sum+arr[i]+arr[i+1]会变大;而是考虑sum+arr[i]和arr[i]的大小关系; 依旧要考虑如何更新左边界
- f(i) = sum(0…i-1)+arr[i] if sum(0…i-1)+arr[i] > arr[i]
- f(i) = arr[i] if sum(0…i-1)+arr[i] > arr[i]
- 动态规划中,“选还是不选”的思想在这里针对的对象不是当前项arr[i],而是sum(0…i-1)
public class Solution {
public int FindGreatestSumOfSubArray(int[] array) {
int currSum=0, maxSum=Integer.MIN_VALUE;
for(int i=0; i<array.length; i++){
//这句体现了“选还是不选”sum(0...i-1); 同时也是是否更新左边界
currSum = Math.max(currSum + array[i], array[i]);
//是否更新最大值
maxSum = Math.max(currSum, maxSum);
}
return maxSum;
}
}
第二遍做,根据递归版改成循环版; 难点:什么时候更新左边界?
public class Solution {
public int FindGreatestSumOfSubArray(int[] array) {
int currSum=0, maxSum=Integer.MIN_VALUE;
for(int i=0; i<array.length; i++){
currSum = currSum + array[i];//考虑当前项之后的和
maxSum = currSum > maxSum? currSum : maxSum;//每次都更新最大值
if(currSum<=0)//是否需要更新左边界
currSum=0;//更新左边界就是令currSum=0
}
return maxSum;
}
}
第二遍做,感觉难点在于考虑这种情况:sum+arr[i]会减小,但是sum+arr[i]+arr[i+1]会变大,解决方案:更新左边界,令currSum=0; 递归版
public class Solution {
public int FindGreatestSumOfSubArray(int[] array) {
/*
什么时候更新子数组的左边界?
当当前子数组的元素和<=0时,子数组右边界的下一个元素作为子数组的新左边界
*/
return Core(array, 0, 0, Integer.MIN_VALUE);
}
public int Core(int[] arr, int index, int currSum, int maxSum){
//base case
if(index == arr.length)
return maxSum;
//
if(currSum+arr[index]<=0)
//这里注意不要把currSum赋成arr[index+1]了,
//currSum只跟0..index有关,跟index+1无关
//令currSum=0是更新左边界的毕竟操作
return Core(arr, index+1, 0, currSum+arr[index] > maxSum? currSum+arr[index]:maxSum);
else
return Core(arr, index+1, currSum+arr[index], currSum+arr[index] > maxSum? currSum+arr[index]:maxSum);
}
}
动态规划(循环版1)
public class Solution {
public int FindGreatestSumOfSubArray(int[] array) {
/*
连续求了n个数的和, 如果这n个数的和小于等于0, 那么把第n+1个数赋给和. 因为连续的n+1数的和小于等于第n+1个数
如果这n+1个数的和大于0, 那么把第n+1个数加到和上,直到遍历完数组或者和小于等于零才会重置和
注意一点: 前面说"如果这n个数的和小于等于0", 为什么不限制为小于0,而是限制为小于等于0, 这是为了处理array[0]用的, sum初始值是0, 所以需要把array[0]赋值给sum
注意一点: 这个思路没有讨论array[i]值的大小, 只通过观察sum的值进行状态的改变
注意一点: sum是array[i]之前的连续n项和, 也就是根据sum的大小, 决定array[i]是加到sum上还是直接赋值给sum
注意一点: 需要设置一个变量记录当前的最大值, 因为sum并不代表当前的最大值, 只是尽力维持连续n项和大于0
*/
int sum = 0, currMax=Integer.MIN_VALUE;
for(int i=0; i<array.length; i++){
if(sum <= 0)
sum = array[i];
else //sum>0
sum += array[i];
if(sum > currMax)
currMax = sum;
}
return currMax;
}
}
动态规划(循环版2)
public class Solution {
public int FindGreatestSumOfSubArray(int[] array) {
/*
动态规划:
令f(i)表示索引从0到i这段区间中的和的最大值
递推关系
f(i) = f(i-1) + array[i] if f(i-1)>0
f(i) = array[i] if f(i-1)<=0 or i==0
*/
int sum=0, max=Integer.MIN_VALUE;
for(int i=0; i<array.length; i++){
sum = Math.max(sum+array[i], array[i]); // 加sum与不加sum, 体现了动态规划的思想
max = Math.max(sum, max);
}
return max;
}
}
动态规划(递归版)
- 需要控制index, sum, max
- 循环版只用控制sum,max
- 递归可能会产生大量重复子问题,导致时间效率和空间效率都不高
- 注意在哪里返回
- 注意递归中sum和max的处理, max稍微复杂一点
- currMax=Core(array, index+1, sum+array[index], sum+array[index]>max?sum+array[index]:max);
- currMax=Core(array, index+1, array[index], array[index]>max?array[index]:max);
public class Solution {
public int FindGreatestSumOfSubArray(int[] array) {
/*
动态规划:
令f(i)表示索引从0到i这段区间中的和的最大值
递推关系
f(i) = f(i-1) + array[i] if f(i-1)>0
f(i) = array[i] if f(i-1)<=0 or i==0
*/
return Core(array, 0, 0, Integer.MIN_VALUE);
}
public int Core(int[] array, int index, int sum, int max){
//recursion finish
if(index==array.length)
return max;
//
int currMax;
if(index==0){
currMax=Core(array, index+1, array[index], array[index]); //注意区分index++和index+1, index++会改变index的值, index+1不会改变index的值
}
else{ //index>0
if(sum>0)
currMax=Core(array, index+1, sum+array[index], sum+array[index]>max?sum+array[index]:max);
else // sum<=0
currMax=Core(array, index+1, array[index], array[index]>max?array[index]:max);
}
return currMax;
}
}