Problem: 53. 最大子数组和
题目:给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组是数组中的一个连续部分。
LeetCode 热题 100】53. 最大子数组和——(解法一)前缀和
整体思路
这段代码同样旨在解决 “最大子数组和” 问题。它采用的是一种非常经典且标准的 动态规划(Dynamic Programming) 方法,通常被称为 Kadane’s 算法 的一种实现形式。
算法的核心思想是定义一个状态 dp[i]
,并找出状态之间的转移关系。
-
状态定义:
dp[i]
被定义为:以nums[i]
结尾的所有连续子数组中,和最大的是多少。- 这个定义是算法的关键。它关注的是以特定位置为终点的子数组,而不是全局的所有子数组。
-
状态转移方程:
- 对于
dp[i]
,我们有两种选择来构成以nums[i]
结尾的最大和子数组:
a. 自成一派:子数组只包含nums[i]
本身。
b. 与前面合并:将nums[i]
接在以nums[i-1]
结尾的最大和子数组(即dp[i-1]
)的后面。 - 我们应该选择哪种呢?这取决于
dp[i-1]
的值:- 如果
dp[i-1] > 0
,说明前面的子数组和是正数,将其与nums[i]
合并会使和增大。此时dp[i] = dp[i-1] + nums[i]
。 - 如果
dp[i-1] <= 0
,说明前面的子数组和是负数或零,将其与nums[i]
合并只会拖后腿,不如让nums[i]
自己重新开始。此时dp[i] = nums[i]
。
- 如果
- 综合以上两种情况,我们可以得到状态转移方程:
dp[i] = Math.max(dp[i-1], 0) + nums[i]
或者等价地写成:
dp[i] = nums[i] + (dp[i-1] > 0 ? dp[i-1] : 0)
- 对于
-
最终结果:
- 在计算出所有
dp[i]
的值之后,全局的最大子数组和ans
就是所有dp[i]
值中的最大值。 - 这是因为全局的最大子数组必然会以某个
nums[i]
为结尾,所以它一定在dp[0], dp[1], ..., dp[n-1]
中出现过。
- 在计算出所有
-
算法步骤:
- 创建一个
dp
数组来存储每个状态的值。 - 基础情况:
dp[0]
只能是nums[0]
本身,因为以nums[0]
结尾的子数组只有一个。 - 迭代计算:通过一个
for
循环,从i=1
开始,利用状态转移方程计算出所有的dp[i]
。 - 记录全局最大值:在每次计算出
dp[i]
后,都用它来更新全局最大值ans
。 - 返回
ans
。
- 创建一个
完整代码
class Solution {
/**
* 找到一个具有最大和的连续子数组,并返回其和。
* @param nums 整数数组
* @return 最大子数组和
*/
public int maxSubArray(int[] nums) {
// ans: 用于存储最终结果,即全局的最大子数组和。
int ans = 0;
int n = nums.length;
// dp 数组:dp[i] 表示以 nums[i] 结尾的连续子数组的最大和。
int[] dp = new int[n];
// 基础情况:以 nums[0] 结尾的子数组只有一个,即 nums[0] 本身。
dp[0] = nums[0];
// 初始化 ans 为 dp[0],因为至少有一个子数组。
ans = dp[0];
// 从第二个元素开始,应用状态转移方程
for (int i = 1; i < n; i++) {
// 状态转移方程:
// dp[i] 的值取决于以 nums[i-1] 结尾的最大和 (dp[i-1]) 是否为正。
// 如果 dp[i-1] > 0,则将其加到 nums[i] 上,可以使和变大。
// 如果 dp[i-1] <= 0,则将其舍弃,从 nums[i] 重新开始。
// Math.max(dp[i - 1], 0) 巧妙地实现了这一逻辑。
dp[i] = Math.max(dp[i - 1], 0) + nums[i];
// 全局最大和 ans 是所有 dp[i] 中的最大值。
// 在每次计算出 dp[i] 后,都用它来更新 ans。
ans = Math.max(ans, dp[i]);
}
// 返回计算出的全局最大子数组和
return ans;
}
}
时空复杂度
时间复杂度:O(N)
- 循环:算法的核心是一个
for
循环,它从i=1
遍历到n-1
。算上初始化的dp[0]
,整个nums
数组被访问了一次。循环执行了N-1
次。 - 循环内部操作:
- 在循环的每一次迭代中,执行的都是基本的数组访问、
Math.max
和加法运算。 - 这些操作的时间复杂度都是 O(1)。
- 在循环的每一次迭代中,执行的都是基本的数组访问、
综合分析:
算法由 N
次 O(1) 的操作组成。因此,总的时间复杂度是 N * O(1)
= O(N)。
空间复杂度:O(N)
- 主要存储开销:算法创建了一个名为
dp
的整型数组来存储动态规划的状态。 - 空间大小:该数组的长度与输入数组
nums
的长度N
相同。 - 其他变量:
ans
,n
,i
等变量都只占用常数级别的空间,即 O(1)。
综合分析:
算法所需的额外空间主要由 dp
数组决定。因此,其空间复杂度为 O(N)。
空间优化提示:
观察状态转移方程 dp[i] = Math.max(dp[i - 1], 0) + nums[i]
,可以发现 dp[i]
的计算只依赖于 dp[i-1]
。因此,我们不需要存储整个 dp
数组,只需一个变量来记录前一个状态即可。这样可以将空间复杂度优化到 O(1)。
参考灵神