LeetCode 53. 最大子数组和【动态规划、贪心、分治】javascript

671 阅读1分钟

53. 最大子数组和

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。

示例 1:

输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6 。

示例 2:

输入: nums = [1]
输出: 1

示例 3:

输入: nums = [5,4,-1,7,8]
输出: 23

提示:

  • 1 <= nums.length <= 105
  • -104 <= nums[i] <= 104

解法一:暴力解法

算法步骤:

子序列下标(i,j),初始值i = 01.  i不变,j=i,j逐步+1,求出当前i下,所有子序列的和,并找到其中最大值。
2.  i+1,重复上述步骤。
3.  全部遍历完成,相当于暴力求和了所有子序列和,最后得到最大值。

小优化点: thisSum在每次j循环之前,置为0。步骤一循环时,例如i=0,j=3时,可以保留(0,3)的和,下一次循环,j+1时,j变为4,求(0,4)的和,直接用上一次(0,3)的和 ,加上 nums[4]即可,不用再重新算一遍0到4的和。

时间复杂度: O(n^2)

实际leetcode解题,用这个解法,数组长度很大的话,时间会超出限制,不能通过。

function maxSubArray(nums: number[]): number {
    const len = nums.length;
    if(len === 1) return nums[0]
    let max = nums[0]
    for(let i = 0; i < len; i++){
        let thisSum = 0;
        for(let j =i; j< len; j++){
            thisSum = thisSum + nums[j]
            max = Math.max(thisSum, max)
        }
    }
    return max
};

解法二:贪心算法

算法步骤:

从左到右遍历数组,sum用于子序列求和,max用于记录最大值。初始sum0max为数组第一个值。
1. 如果 sum > 0,则说明 sum 对结果有增益效果,则sum 保留并加上当前遍历数字
2. 如果 sum <= 0,则说明 sum 对结果无增益效果,需要舍弃,则 sum 直接更新为当前遍历数字
3. 每次比较 summax的大小,将最大值置为max,遍历结束返回结果

时间复杂度: O(n)

function maxSubArray(nums: number[]): number {
    const len = nums.length;
    let sum = 0
    let max = nums[0]
    for(let i =0; i<len; i++){
        if(sum>0){
            sum = sum + nums[i]
        }else{
            sum = nums[i]
        }
        max = Math.max(sum, max)
    }
    return max
};

解法三:动态规划

把问题转化为:以nums[i]结尾的连续子数组的最大和。相当于倒着遍历。

image.png

时间复杂度: O(n)

function maxSubArray(nums: number[]): number {
    const len = nums.length;
    let pre = 0, max = nums[0];
    for(let i = 0; i < len; i++){
        pre = Math.max(pre+ nums[i], nums[i])
        max = Math.max(max, pre)
    }
    
    return max
};

解法四:分治法

算法思路

数组进行分割,平均分成左右两部分。此时有三种情况:
1.最大子序列全部在数组左部分
2.最大子序列全部在数组右部分
3.最大子序列横跨左右数组

对于前两种情况,我们相当于将原问题转化为了规模更小的同样问题。通过再次调用方法,传入起始和结束坐标,递归求解。

对于第三种情况,由于已知循环的起点(即中点),我们只需要进行一次循环,分别找出包含中点左侧第一个元素的左边最大子序列,和包含中点右侧第一个元素的右边的最大子序列。此时序列一定是横跨中点连续的。再将左右最大值相加。

那么,最终计算上面三种情况的最大子序列和, 取出最大的即可。

时间复杂度:  O(nlogn)

function maxSubArray(nums: number[]): number {
  return maxSubArrayDivideWithBorder(nums, 0, nums.length-1);
};

// 分治法核心代码
function maxSubArrayDivideWithBorder(nums: number[], start: number, end: number): number{
  if(start === end) return nums[start]
  const mid = Math.floor((start + end)/2)
  // 完全在左侧的数组求最大
  const leftMax = maxSubArrayDivideWithBorder(nums, start, mid);
  // 完全在右侧的数组求最大
  const rightMax = maxSubArrayDivideWithBorder(nums, mid+1, end);
  // 从分隔位开始,左侧最大和(从中间向左侧连续,至少包含中间这个数)
  // 取左侧循环开始的第一个值,初始化为左侧最大值
  let midMaxLeft = nums[mid]
  let leftSum = 0
  for (let i=mid; i >= start; i--){
    leftSum = leftSum + nums[i]
    if(leftSum>midMaxLeft){
      midMaxLeft = leftSum;
    }
  }

  // 从分隔位开始,右侧最大和(从中间向右侧连续,至少包含中间这个数)
  // 取右侧循环开始的第一个值,初始化为右侧最大值
  let midMaxRight = nums[mid+1];
  let rightSum = 0;
  for (let i = mid+1; i <= end; i++) {
    rightSum = rightSum + nums[i];
    if (rightSum > midMaxRight) {
      midMaxRight = rightSum;
    }
  }
  // 最大子序列横跨左右数组:中间值连续时的最大和
  const midCrossMax = midMaxLeft + midMaxRight;
  return Math.max(midCrossMax, Math.max(leftMax, rightMax));
}