"子序列、子数组、子串,傻傻分不清楚"
开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第4天,点击查看活动详情
53. 最大子数组和 题目描述:给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组 是数组中的一个连续部分。
| 示例 |
|---|
| 输入: 输出: 解释: 连续子数组 的和最大,为 |
题目要我们找出和最大的连续子数组的值是多少(说明元素中必有负数,要分开讨论),【连续】是关键字,连续很重要,不是子序列。
暴力解法
1. 暴力 O(N^3)
纯暴力很好理解啦,就是枚举出所有的可能,然后放在一起比大小。(💥妥妥超时)
/**
* 空间复杂度 O(1)
* 时间复杂度 O(n^3),n是nums数组的长度
*/
function maxSubArray(nums: number[]): number {
let ans = Number.MIN_SAFE_INTEGER;
const len = nums.length;
for(let i = 0; i < len; i++) {
for(let j = i; j < len; j++) {
ans = Math.max(ans, addRange(nums, i, j));
}
}
return ans;
};
function addRange(nums: number[], left: number, right: number): number {
let sum = 0;
while(left <= right) {
sum += nums[left];
left += 1;
}
return sum;
}
2. 暴力 O(N^2)
暴力遍历算法的时间复杂度过高,可以优化:保存 连续区域的子数组元素之和,记为 ,这样在计算 连续区域的子数组元素之和时,只需要计算 即可,即 。这样时间复杂度可从 降低值 ,可惜 💥依然超时。
/**
* 空间复杂度 O(1)
* 时间复杂度 O(n^2),n是nums数组的长度
*/
function maxSubArray(nums: number[]): number {
let ans = Number.MIN_SAFE_INTEGER;
const len = nums.length;
for(let i = 0; i < len; i++) {
let sum = nums[i];
for(let j = i + 1; j < len; i++) {
sum += nums[j];
ans = Math.max(ans, sum);
}
}
return ans;
};
中规中矩的动态规划
-
最值问题? 求 连续子数组 的元素之和最大
-
无后效行? 没有要求指出哪个连续子数组的元素之和最大
-
最值可以穷举? 暴力法就是最好的证明
-
最优子结构? 假设我们知道以 结尾(一定包括这个元素)的连续子数组的元素最大和为 ,通过 正负性的判断,可以得知以 结尾的连续子数组的元素最大和
1、确定 dp 状态数组
设 就是以 结尾的子数组最大和,其中 。
2、确定 dp 状态转移方程
对于 的符号,一定有三种情况,分别是 , 以及 ;对于 的符号,也一定有三种情况,分别是 , 以及 。故我们对 种情况分别进行讨论:
当 时,
💥 NOTE:
-
当 时, 和 相互正向影响,故要在 基础上累加! ,方能保证 是以 结尾的子数组的最大元素之和;
-
当 时, 与上式等价;
-
当 时, 对 是负向影响,故要将 舍去,仅保留 保证连续,还能保证 是以 结尾的子数组的最大元素之和。
当 时,
💥 NOTE:
-
当 时, 成立么❓ 答案是否定的,走到哪里都不能忘记初心,就是 状态定义, 不能没有 的参与;
-
当 时,两者都是 ,相当于白忙乎了, 或 均可,同理 不能没有 的参与;
-
当 时, 对 是负向影响,故要将 舍去,仅保留 保证连续,还能保证 是以 结尾的子数组的最大元素之和。
当 时,
💥NOTE:
-
当 时, 对 是正向影响,故要在 基础上累加 ,方能保证 是以 结尾的子数组的最大元素之和;
-
当 时, 或 ,同理 不能没有 的参与;
-
当 时, 对 是负向影响,故要将 舍去,仅保留 保证连续,还能保证 是以 结尾的子数组的最大元素之和。
综上所述,
3、确定 dp 初始状态
就是以 结尾的子数组最大和,即
4、确定遍历顺序
从 遍历到 即可。
5、确定最终返回值
重温初心: 是以 结尾的子数组最大和,故 仅仅是以 元素结尾的子数组的最大和,并不代表全局的最大子数组之和,故需要全局对比,即,。
NOTE: 代表将数组中所有元素按照其索引按顺序传到 函数。
6、代码示例
/**
* 空间复杂度 O(n),n是nums数组的长度
* 时间复杂度 O(n)
*/
function maxSubArray(nums: number[]): number {
const length = nums.length;
const dp = Array.from({ length }, () => 0);
dp[0] = nums[0];
for(let i = 1; i < length; i++){
dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
}
return Math.max(...dp);
};
局部优化:一遍计算 状态数组,一遍计算并保存最大值。
/**
* 空间复杂度 O(n),n是nums数组的长度
* 时间复杂度 O(n)
*/
function maxSubArray(nums: number[]): number {
const length = nums.length;
const dp = Array.from({ length }, () => 0);
let ans = dp[0] = nums[0];
for(let i = 1; i < length; i++){
dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
ans = Math.max(ans, dp[i]);
}
return ans;
}
空间复杂度优化: 仅与 相关,故可将其状态数组进行压缩。
/**
* 空间复杂度 O(1)
* 时间复杂度 O(n),n是nums数组的长度
*/
function maxSubArray(nums: number[]): number {
let dp = nums[0];
let ans = nums[0];
for(let i = 1, len = nums.length; i < len; i++){
dp = Math.max(dp + nums[i], nums[i]);
ans = Math.max(ans, dp);
}
return ans;
}
思维提升-前缀和
1. 暴力前缀和
观察【法1:纯暴力求解】思路,每次计算 连续区域子数组元素之和时,都是通过循环遍历累加求得;后来发现可以缓存 子数组元素之和,目的是可以 时间级别计算出 子数组元素之和。
换一个思路,记数组 ,其中 代表 连续区域子数组元素之和。此时 连续区域子数组元素之和为 。
其中,。
/**
* 空间复杂度 O(n),n是nums数组的长度
* 时间复杂度 O(n^2)
*/
function maxSubArray(nums: number[]): number {
const len = nums.length;
const prefixSums = new Array(len).fill(nums[0]);
for (let i = 1; i < len; i++) {
prefixSums[i] = prefixSums[i - 1] + nums[i];
}
let maxSum = Number.MIN_SAFE_INTEGER;
for(let i = 0; i < len; i++) {
for(let j = i; j < len; j++) {
maxSum = Math.max(maxSum, prefixSums[j] - prefixSums[i] + nums[i]);
}
}
return maxSum;
};
2. 时间优化
前缀和(暴力版)与【法1:纯暴力求解 那个算法】有异曲同工之妙,但是时间与空间复杂度依然没有得到优化。换一个角度思考问题:我们在计算前缀和时,可以使用两个临时变量,即
:前缀和最小值,计算方法:当前 与当前前缀和 的最小值,即,
:前缀和最大值,计算方法:当前 与当前前缀和 与 之差的最大值,即,
/**
* 空间复杂度 O(n),n是nums数组的长度
* 时间复杂度 O(n)
*/
function maxSubArray(nums: number[]): number {
const prefixSums = new Array(nums.length).fill(nums[0]);
let maxPrefixSum = nums[0];
let minPrefixSum = 0;
for(let i = 1; i < nums.length; i++){
prefixSums[i] = prefixSums[i - 1] + nums[i];
minPrefixSum = Math.min(minPrefixSum, prefixSums[i - 1]);
maxPrefixSum = Math.max(maxPrefixSum, prefixSums[i] - minPrefixSum);
}
return maxPrefixSum;
};
3. 空间优化
空间复杂度优化: 仅与 相关,故可将其状态数组进行压缩。
/**
* 空间复杂度 O(1)
* 时间复杂度 O(n),n是nums数组的长度
*/
function maxSubArray(nums: number[]): number {
let currSum = nums[0];
let maxSum = nums[0];
let minSum = 0;
for(let i = 1; i < nums.length; i++){
minSum = Math.min(minSum, currSum);
currSum = currSum + nums[i];
maxSum = Math.max(maxSum, currSum - minSum);
}
return maxSum;
};