嗨!~ 大家好,我是YK菌 🐷 ,一个微系前端 ✨,爱思考,爱总结,爱记录,爱分享 🏹,欢迎关注我呀 😘 ~ [微信号:
yk2012yk2012,微信公众号:ykyk2012]
「这是我参与11月更文挑战的第29天,活动详情查看:2021最后一次更文挑战」
其实这题我三年前在我的公众号里有写过 分而治之方法解决最大子列和问题——还有更快 以及 最快处理最大子列和问题——在线处理 那个时候还是用的C语言,今天我们使用JavaScript来再探索一下这道题吧~ 其实解题不是目的,我们真正的目的是通过这道题,学习一些算法思想,学习一些解决问题的方法和技巧!
53. 最大子序和
给定一个整数数组
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;
};
测试是对的
但是效率太低,无法通过所有测试用例
这里面一共用到了三次循环,所以他的时间复杂度为
其实再仔细想想,需要全部这样逐个求和吗?是不是存在许多重复求和,可不可以优化一下呢?
② 进步:优化求和
我们把第三次循环进行变换, 第三次循环其实没有必要, 直接在前面的基础上加上后面一个元素,就可以得到新的子序和
进行改进后的代码
/**
* @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;
};
可以看出,它的时间复杂度只有 当然,这样也是跑不了LeetCode
其实看到,马上反应的就是:把它降到 怎么做呢?
③ 分而治之
我们采用的算法思想就是——分而治之
1) 把它分成两个或多个更小的问题;
2) 分别解决每个小问题;
3) 把各小问题的解答组合起来,即可得到原问题的解答。
放到这题,我们具体的做法就是:把这段数列先一分为二,然后在子数列里再进行一分为二。将一个大问题化解成许多的小问题来处理,然后再分别跨域各自分界线,将问题组合起来。
我们来举一个具体的例子吧,先给一个数列
以上将它对分对分(以取左边为例)
最大子列和为4,其他类似,我们得到
再跨越中间分界线
左边的最大子列和是6,右边类似
最后再在中间求最大子列和得到结果
最大子列和为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))
}
终于能跑出来了,就是效率还是不行
④ 分治优化:线段树
我们进行优化,分治还有一种方法 线段树求解最长公共上升子序列问题 ,我们直接看代码
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;
};
这里的时间复杂度是 空间复杂度
⑤ 在线处理
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;
};
这里的时间复杂度是, 空间复杂度是