1、题目描述
给你一个整数数组
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
2、解法
方法一:暴力
bash
import java.util.ArrayList;
import java.util.Collections; // 需要导入 Collections 来使用 max 方法
class Solution {
public int maxSubArray(int[] nums) {
ArrayList<Integer> result = new ArrayList<>();
// 外层循环:确定子数组的起始位置
for (int i = 0; i < nums.length; i++) {
int sum = 0; // 每次换起始位置时,重置和
// 内层循环:确定子数组的结束位置,并累加求和
for (int j = i; j < nums.length; j++) {
sum += nums[j]; // 累加当前元素
result.add(sum); // 每累加一次,就得到了一个以 i 开头、j 结尾的子数组的和,存入列表
}
}
//使用 Collections.max() 来获取列表中的最大值
return Collections.max(result);
}
}
这种方法虽然可以,但是时间复杂度为O(n^2),当数据非常大的时候,会超出内存限制。
方法二:进阶版
虽然上面的代码可以通过部分测试,但使用 ArrayList 存储所有的和会消耗大量内存(空间复杂度较高)。其实不需要把所有和都存下来,只需要用一个变量 max 记录当前遇到的最大值即可。
bash
class Solution {
public int maxSubArray(int[] nums) {
// 初始化最大值为整数最小值,防止数组全是负数时出错
int max = Integer.MIN_VALUE;
for (int i = 0; i < nums.length; i++) {
int sum = 0;
for (int j = i; j < nums.length; j++) {
sum += nums[j];
// 每次算出新和,直接比较并更新最大值
if (sum > max) {
max = sum;
}
}
}
return max;
}
}
这种呢,也能通过,虽然比上面第一种方法通过的多一点,但是不能全部通过案例
方法三:动态规划
动态规划是一种通过将复杂问题分解为更简单的子问题,并存储这些子问题的解以避免重复计算,从而高效求解最优化问题的算法设计方法。 ------ 源自《算法导论》(Introduction to Algorithms, CLRS)
其核心基于两个数学性质:
最优子结构(Optimal Substructure) 问题的最优解包含其子问题的最优解。
重叠子问题(Overlapping Subproblems) 在递归求解过程中,相同的子问题被多次重复计算。
动态规划设计的核心知识点(标准步骤):
1. 定义状态(State)
用一个变量或数组
dp[i]、dp[i][j]等表示子问题的解。例如:
dp[i]表示"前 i 个元素中能获得的最大和"。2. 确定状态转移方程(Recurrence Relation)
描述当前状态如何由之前的状态推导而来。
例如:
dp[i] = max(dp[i-1] + nums[i], nums[i])(最大子数组和)3. 设定初始条件(Base Case)
最简单子问题的解,作为递推起点。
例如:
dp[0] = nums[0]4. 确定计算顺序(Order of Computation)
通常自底向上(Bottom-up):从小到大填表(如 for 循环)
也可自顶向下(Top-down):递归 + 记忆化(Memoization)
5. 空间优化(可选)
若当前状态只依赖前几个状态,可用滚动变量代替整个数组。
例如:斐波那契数列只需
a, b, c三个变量。
动态规划的两种实现方式:
方式 描述 特点 自底向上(迭代) 从最小的子问题开始,逐步构建到原问题 无递归开销,效率高,常用 自顶向下(递归+记忆化) 从原问题出发,递归分解,用哈希表/数组缓存结果 思路直观,但有函数调用开销
经典 DP 问题类型(体现设计思想):
线性 DP:最大子数组和、打家劫舍
区间 DP:最长回文子串、矩阵链乘
背包 DP:0-1 背包、完全背包
树形 DP:二叉树最大路径和
状态机 DP:股票买卖问题
关键总结(一句话):
动态规划 = 最优子结构 + 重叠子问题 + 状态定义 + 状态转移 + 初始条件 + 计算顺序
简单理解一下
动态规划(Dynamic Programming,简称 DP)是一种解决复杂问题的聪明办法 : 把大问题拆成小问题,先解决小问题,再用小问题的答案一步步推出大问题的答案。
核心思想(3句话):
分阶段:把问题分成一步步(比如从第1天到第n天)。
记答案:把每一步的结果存下来(避免重复算)。
推着走:后面的答案 = 前面的答案 + 当前选择。
举个简单例子:
爬楼梯:每次能爬 1 或 2 阶,问到第 5 阶有几种方法?
到第1阶:1种
到第2阶:2种(1+1 或 直接2)
到第3阶:= 到第2阶的方法 + 到第1阶的方法 = 2 + 1 = 3
到第4阶:= 第3阶 + 第2阶 = 3 + 2 = 5
到第5阶:= 第4阶 + 第3阶 = 5 + 3 = 8
每一步都用前面的结果,不用从头算!
动态规划的两个条件:
有重复的小问题(比如算第5阶时,第3阶被用了好多次)
大问题的最优解 = 小问题的最优解组合起来
一句话总结:
动态规划 = 记住已经算过的,别重复干傻事,从小问题一步步推出大答案
这是解决此问题的标准解法。核心思想是:如果前面的子数组和变成了负数,那就果断丢弃,从当前元素重新开始计算。
- 思路:
- 我们维护两个变量:
- current_sum:以当前元素结尾的连续子数组的最大和。
- max_sum:全局最大的子数组和。
- 对于数组中的每一个数 num,我们需要决定是将它加入之前的子数组,还是把它作为新子数组的开头。
- 如果 current_sum 是负数,加上它只会让结果更小,不如直接从 num 开始。
- 如果 current_sum 是正数,加上 num 可能会更大,那就加上。
- 我们维护两个变量:
bash
class Solution {
public int maxSubArray(int[] nums) {
// 初始化:假设第一个元素就是最大值
int current_sum = nums[0];
int max_sum = nums[0];
// 从第二个元素开始遍历
for (int i = 1; i < nums.length; i++) {
// 核心逻辑:
// 如果 current_sum < 0,抛弃它,从 nums[i] 重新开始
// 如果 current_sum >= 0,保留它,加上 nums[i]
current_sum = Math.max(nums[i], current_sum + nums[i]);
// 更新全局最大值
max_sum = Math.max(max_sum, current_sum);
}
return max_sum;
}
}