【LeetCode】最大子序和从O(N^3)到O(N) - 暴力初探 - 分而治之 - 在线处理

618 阅读5分钟

嗨!~ 大家好,我是YK菌 🐷 ,一个微系前端 ✨,爱思考,爱总结,爱记录,爱分享 🏹,欢迎关注我呀 😘 ~ [微信号: yk2012yk2012,微信公众号:ykyk2012]

「这是我参与11月更文挑战的第29天,活动详情查看:2021最后一次更文挑战

其实这题我三年前在我的公众号里有写过 分而治之方法解决最大子列和问题——还有更快 以及 最快处理最大子列和问题——在线处理 那个时候还是用的C语言,今天我们使用JavaScript来再探索一下这道题吧~ 其实解题不是目的,我们真正的目的是通过这道题,学习一些算法思想,学习一些解决问题的方法和技巧!

53. 最大子序和

leetcode-cn.com/problems/ma…

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

① 初探:暴力求和

我们可以想到最简单最暴力的方法,就是直接对所有子列依次求和,然后逐个比较,取最大的即为最大子列和。

/**
 * @param {number[]} nums
 * @return {number}
 */
var maxSubArray = function (nums) {
  let thisSum = -Infinity;
  let maxSum = -Infinity;

  for (let i = 0; i < nums.length; i++) {
    for (let j = i; j < nums.length; j++) {
      thisSum = 0; // nums[i] 到 nums[j]的子序和
      for (let k = i; k <= j; k++) {
        thisSum += nums[k];
        if (thisSum > maxSum) { // 如果刚得到的这个子列和更大
          maxSum = thisSum; // 则更新结果
        }
      }
    }
  }

  return maxSum;
};

测试是对的

image.png

但是效率太低,无法通过所有测试用例

image.png

这里面一共用到了三次循环,所以他的时间复杂度为 O(N3)O(N^{3})

其实再仔细想想,需要全部这样逐个求和吗?是不是存在许多重复求和,可不可以优化一下呢?

② 进步:优化求和

我们把第三次循环进行变换, 第三次循环其实没有必要, 直接在前面的基础上加上后面一个元素,就可以得到新的子序和

进行改进后的代码

/**
 * @param {number[]} nums
 * @return {number}
 */
var maxSubArray = function(nums) {
  let thisSum = -Infinity;
  let maxSum = -Infinity;

  for (let i = 0; i < nums.length; i++) {
    thisSum = 0; // nums[i] 到 nums[j]的子序和
    for (let j = i; j < nums.length; j++) {
      thisSum += nums[j];
      // 对于相同的i,不同的j,只要在j-1次循环的基础上累加1项即可
      if (thisSum > maxSum) {// 如果刚得到的这个子序和更大
        maxSum = thisSum; // 则更新结果
      }
    }
  }

  return maxSum;

};

可以看出,它的时间复杂度只有 O(N2)O(N^{2}) 当然,这样也是跑不了LeetCode

其实看到O(N2)O(N^{2}),马上反应的就是:把它降到 O(NlogN)O(NlogN) 怎么做呢?

③ 分而治之

我们采用的算法思想就是——分而治之
1) 把它分成两个或多个更小的问题;
2) 分别解决每个小问题;
3) 把各小问题的解答组合起来,即可得到原问题的解答。

放到这题,我们具体的做法就是:把这段数列先一分为二,然后在子数列里再进行一分为二。将一个大问题化解成许多的小问题来处理,然后再分别跨域各自分界线,将问题组合起来。

image.png

我们来举一个具体的例子吧,先给一个数列

image.png

以上将它对分对分(以取左边为例)

image.png

image.png

最大子列和为4,其他类似,我们得到

image.png

再跨越中间分界线

image.png

左边的最大子列和是6,右边类似

image.png

最后再在中间求最大子列和得到结果

image.png

最大子列和为11

编码实现如下

/**
 * @param {number[]} nums
 * @return {number}
 */
function maxSubArray(nums) {
    let len = nums.length

    if(len === 0){
        return 0
    }

    return maxSubArraySum(nums, 0, len - 1)
};

// 计算交叉的最大子序和
function maxCrossingSum(nums, left, mid, right){
    let sum = 0
    let leftSum = -Infinity
    for(let i = mid; i >= left; i--){
        sum += nums[i]
        if(sum > leftSum){
            leftSum = sum
        }
    }

    sum = 0
    let rightSum = -Infinity
    for(let i = mid + 1; i <= right; i++){
        sum += nums[i]
        if(sum > rightSum){
            rightSum = sum
        }
    }
    return (leftSum + rightSum)
}

// 计算最大子序和函数【递归】
function maxSubArraySum(nums, left, right){

    if(left === right){
        return nums[left]
    }

    let mid = Math.floor((left + right)/2)

    let leftSum = maxSubArraySum(nums, left, mid)
    let rightSum = maxSubArraySum(nums, mid+1, right)
    let crossSum = maxCrossingSum(nums, left, mid, right)

    return max3(leftSum, rightSum, crossSum)
}

// 返回3个数中的最大值
function max3(num1, num2, num3){
    return Math.max(num1, Math.max(num2, num3))
}

终于能跑出来了,就是效率还是不行

image.png

④ 分治优化:线段树

我们进行优化,分治还有一种方法 线段树求解最长公共上升子序列问题 ,我们直接看代码

function Status(lSum, rSum, mSum, iSum) {
    this.lSum = lSum; // lSum 表示 [l,r] 内以 l 为左端点的最大子段和
    this.rSum = rSum; // rSum 表示 [l,r] 内以 r 为右端点的最大子段和
    this.mSum = mSum; // mSum 表示 [l,r] 内的最大子段和
    this.iSum = iSum; // iSum 表示 [l,r] 的区间和
}

function pushUp(lSub, rSub){

    // iSum 就是 左子区间和 + 右子区间的和
    const iSum = lSub.iSum + rSub.iSum;
    
    // lSum 就是 左子区间的lSum 和 左区间
    const lSum = Math.max(lSub.lSum, lSub.iSum + rSub.lSum);
    
    // rSum 表示 [l,r] 内以 r 为右端点的最大子段和
    const rSum = Math.max(rSub.rSum, rSub.iSum + lSub.rSum);
    
    // mSum 表示 [l,r] 内的最大子段和
    const mSum = Math.max(Math.max(lSub.mSum, rSub.mSum), lSub.rSum + rSub.lSum);
    
    return new Status(lSum, rSum, mSum, iSum);
}

// 查询 a 序列 [l,r] 区间内的最大子段和
function getInfo(a, l, r){
    // 递归终止条件
    if (l === r) {
        return new Status(a[l], a[l], a[l], a[l]);
    }
    
    // 下面是"分"的过程
    // 得到中间值
    const m = (l + r) >> 1;
    // 分治:求左边
    const lSub = getInfo(a, l, m);
    // 分治:求右边
    const rSub = getInfo(a, m + 1, r);
    
    return pushUp(lSub, rSub);
}

var maxSubArray = function(nums) {
    return getInfo(nums, 0, nums.length - 1).mSum;
};

这里的时间复杂度是O(N)O(N) 空间复杂度 O(logN)O(logN)

image.png

⑤ 在线处理

thisSum 维护一个 向右累加 子序和 如果之前和子序和都没有第i个元素大,就从i开始重新维护一个 累加子序和

maxSum 保存遍历过程中的最大子序和

/**
 * @param {number[]} nums
 * @return {number}
 */
var maxSubArray = function(nums) {
    // 当前子序和
    let thisSum = 0;
    // 最大子序和
    let maxSum = nums[0];
    
    // 遍历一遍序列
    for(let i = 0; i < nums.length; i++){
    
        if(thisSum + nums[i] < nums[i]){
            thisSum = nums[i];
        }else{
            thisSum += nums[i];
        }
        
        if(thisSum > maxSum){
            maxSum = thisSum;
        }
    }
    
    return maxSum;
};

精简一下代码

/**
 * @param {number[]} nums
 * @return {number}
 */
var maxSubArray = function(nums) {
    let thisSum = 0;
    let maxSum = nums[0];
    
    for(let i = 0; i< nums.length; i++){
        thisSum = Math.max(thisSum + nums[i], nums[i]);
        maxSum = Math.max(maxSum, thisSum);
    }
    
    return maxSum;
};

这里的时间复杂度是O(N)O(N), 空间复杂度是O(1)O(1)

在这里插入图片描述