CS-Notes 剑指 Offer 42:连续子数组的最大和——单趟贪心算法的推导、实现与边界处理
本篇基于 CS-Notes 仓库中的剑指 Offer 题解文档,系统讲解"连续子数组的最大和"这一经典最大子数组问题:从题目定义出发,完整还原原仓库给出的 O(n) 单趟贪心解法(Kadane 算法)的每一行语义,逐步在示例数组上推演执行过程,并从动态规划角度证明其正确性,最后梳理全负数组、空数组等边界场景的处理要点。读完之后,你能够独立写出该题的 O(n) 时间、O(1) 空间实现,并说清"前缀和一旦非正就必须重置"这一贪心准则背后的数学依据。
1. 问题定义
题解文档给出的原始示例如下(原文见 [notes/42. 连续子数组的最大和.md](https://gitcode.com/GitHub_Trending/cs/CS-Notes/blob/b70121d377cb6005eb65f12b098cd5decd905669/notes/42. 连续子数组的最大和.md?utm_source=gitcode_repo_files)):
{6, -3, -2, 7, -15, 1, 2, 2},连续子数组的最大和为 8(从第 0 个开始,到第 3 个为止)。
形式化表述:给定一个整数数组 nums,在下标连续的子数组(subarray)中,找出元素和最大的一个,返回其和。注意三个约束容易被忽略:
- "连续"是硬约束——不能跳着选元素,因此不能退化成"选所有正数之和";
- 子数组非空,至少要包含一个元素,所以全负数组的答案是最大的那个负数而不是 0;
- 题目要求的是"最大和"本身,经典进阶变体还要求输出子数组的起止下标(本仓库题解仅要求和,下标信息可在遍历中额外维护)。
该题收录于 CS-Notes 的剑指 Offer 题解体系,在 [notes/剑指 Offer 题解 - 目录.md](https://gitcode.com/GitHub_Trending/cs/CS-Notes/blob/b70121d377cb6005eb65f12b098cd5decd905669/notes/剑指 Offer 题解 - 目录.md?utm_source=gitcode_repo_files#L92-L103) 的"动态规划"分类下,与斐波那契、礼物的最大价值、最长不含重复字符的子字符串等题并列。原文档同时给出了牛客网对应的练习题链接,可在题解目录顶部找到刷题入口的说明。
2. 核心思路:前缀和一旦非正就"弃前"
原仓库给出的参考实现(完整继承自原文档,未作改动):
public int FindGreatestSumOfSubArray(int[] nums) {
if (nums == null || nums.length == 0)
return 0;
int greatestSum = Integer.MIN_VALUE;
int sum = 0;
for (int val : nums) {
sum = sum <= 0 ? val : sum + val;
greatestSum = Math.max(greatestSum, sum);
}
return greatestSum;
}
这段代码的全部决策浓缩在 sum = sum <= 0 ? val : sum + val; 一行。逐行拆解:
greatestSum初始化为Integer.MIN_VALUE而不是 0——这是关键细节:它保证当数组元素全为负数时,答案能被更新为"最大的那个负数",而不是错误地停在 0;sum表示"以当前元素val作为右端点的、最优的连续子数组和"。如果走到这一步时它已经<= 0,说明把它拼接到任何后续子数组上只会拖后腿(贡献非正),那么从val重新开始一定更优;- 每处理一个元素,就用
sum更新全局最大值greatestSum,即"以每个位置为右端点的最优和"取全集最大。
贪心准则的正确性可以用反证法说明:假设存在最优子数组 [l..r],而 nums[l] + ... + nums[l+k] <= 0(前缀非正)。那么 [l+k+1..r] 的和严格不小于 [l..r] 的和,即把非正前缀砍掉不会更差。反复砍除所有非正前缀后,任何最优子数组都可以保证"从其左端点起的前缀和恒为正",这正是 sum <= 0 就重置、sum > 0 就续接的策略所枚举的空间,因此贪心不会错过最优解。
3. 动态规划视角:原实现就是滚动优化的 DP
把上述准则翻译成 DP 语言,状态定义更清晰:
- 状态:
dp[i]= 以nums[i]结尾的连续子数组的最大和; - 转移:
dp[i] = max(nums[i], dp[i-1] + nums[i]); - 答案:
max(dp[i])(对全部i)。
转移的含义与前文一致:要么"从 i 新开一段",要么"接在 i-1 的最优段后面"。标准写法需要 O(n) 空间:
public int findMaxSubArrayDP(int[] nums) {
int dp = nums[0];
int greatestSum = nums[0];
for (int i = 1; i < nums.length; i++) {
dp = Math.max(nums[i], dp + nums[i]); // 滚动变量,只依赖上一状态
greatestSum = Math.max(greatestSum, dp);
}
return greatestSum;
}
dp 只依赖前一个状态,滚动成单个变量后空间降为 O(1)——这就是原仓库代码的数学本质:sum 即滚动 dp,greatestSum 即答案。作为对照,朴素做法是枚举所有 O(n²) 个子区间分别求和(O(n³)),或者用前缀和把单次区间和降到 O(1) 从而得到 O(n²);贪心/Dual 一趟扫描的 O(n) 已是该问题在线性比较模型下的最优量级。
4. 示例数组的逐步推演
用原题目示例 {6, -3, -2, 7, -15, 1, 2, 2} 逐元素执行原实现,验证"最大和为 8(从第 0 个开始,到第 3 个为止)":
| 步骤 | val | 进入时 sum | 决策 | 处理后 sum | greatestSum |
|---|---|---|---|---|---|
| 1 | 6 | 0 | sum<=0,重置 | 6 | 6 |
| 2 | -3 | 6 | 续接 | 3 | 6 |
| 3 | -2 | 3 | 续接 | 1 | 6 |
| 4 | 7 | 1 | 续接 | 8 | 8 |
| 5 | -15 | 8 | 续接 | -7 | 8 |
| 6 | 1 | -7 | sum<=0,重置 | 1 | 8 |
| 7 | 2 | 1 | 续接 | 3 | 8 |
| 8 | 2 | 3 | 续接 | 5 | 8 |
可以看到两次"重置"时刻:第 5 个元素把累计和打到 -7 后,第 6 个元素 1 触发了 sum <= 0 ? val : sum + val 中的重置分支;最终最大值 8 产生于下标 0~3 的区间 [6, -3, -2, 7],与题目描述完全吻合。
5. 边界场景与正确性细节
- 全负数组,如
{-3, -2, -5}:每个位置都会触发重置,greatestSum依次被更新为 -3、-2,最终返回 -2(最大的单个负数)。若把greatestSum错误初始化为 0,这里就会返回 0,与"子数组非空"的语义矛盾——原实现用Integer.MIN_VALUE精确规避了这一点; - 空数组 / null 输入:原实现约定返回 0,属于接口层面的防御性处理,避免解引用异常;
- 含 0 的数组:
sum <= 0与sum < 0两种写法都能得到正确答案。选<= 0时,值为 0 的前缀会被视为"无益前缀"而弃掉,语义上更干净(前缀贡献为 0 时续接与重置等价,但重置避免了无谓的依赖); - 单元素数组:循环执行一次,直接返回该元素,无需特判。
6. 仓库内的同思路题解与延伸
CS-Notes 的题解体系对这道题的定位是"动态规划"(见 [notes/剑指 Offer 题解 - 目录.md](https://gitcode.com/GitHub_Trending/cs/CS-Notes/blob/b70121d377cb6005eb65f12b098cd5decd905669/notes/剑指 Offer 题解 - 目录.md?utm_source=gitcode_repo_files#L92-L103)),其分类下同批题目如 [notes/10.1 斐波那契数列.md](https://gitcode.com/GitHub_Trending/cs/CS-Notes/blob/b70121d377cb6005eb65f12b098cd5decd905669/notes/10.1 斐波那契数列.md?utm_source=gitcode_repo_files)、[notes/47. 礼物的最大价值.md](https://gitcode.com/GitHub_Trending/cs/CS-Notes/blob/b70121d377cb6005eb65f12b098cd5decd905669/notes/47. 礼物的最大价值.md?utm_source=gitcode_repo_files)、[notes/49. 丑数.md](https://gitcode.com/GitHub_Trending/cs/CS-Notes/blob/b70121d377cb6005eb65f12b098cd5decd905669/notes/49. 丑数.md?utm_source=gitcode_repo_files)、[notes/60. n 个骰子的点数.md](https://gitcode.com/GitHub_Trending/cs/CS-Notes/blob/b70121d377cb6005eb65f12b098cd5decd905669/notes/60. n 个骰子的点数.md?utm_source=gitcode_repo_files) 都是"状态 + 转移"的标准 DP 训练,可与本文的滚动 DP 对照阅读。
更重要的是,仓库里有两道题与本题共享"单趟扫描 + 维护一个滚动状态变量"的骨架,值得对比掌握:
- [notes/63. 股票的最大利润.md](https://gitcode.com/GitHub_Trending/cs/CS-Notes/blob/b70121d377cb6005eb65f12b098cd5decd905669/notes/63. 股票的最大利润.md?utm_source=gitcode_repo_files):同样是一次遍历,但滚动状态是"迄今为止的最低买入价"
soFarMin,每次尝试以当前价为卖出价计算收益并取最大——与本题"滚动最优前缀和 vs 当前最优全局和"的双变量结构几乎同构; - [notes/59. 滑动窗口的最大值.md](https://gitcode.com/GitHub_Trending/cs/CS-Notes/blob/b70121d377cb6005eb65f12b098cd5decd905669/notes/59. 滑动窗口的最大值.md?utm_source=gitcode_repo_files):同样是数组扫描问题,但它把窗口约束显式化,用大顶堆维护窗口内最大值,时间复杂度 O(N log M)——可作为"当 O(n) 贪心不可用时引入辅助数据结构"的对照样本。
7. 复杂度总结
以数组长度为 n 计,原实现的复杂度为:
| 指标 | 数值 | 说明 |
|---|---|---|
| 时间 | O(n) | 单次遍历,每元素 O(1) |
| 空间 | O(1) | 仅 sum、greatestSum 两个标量(滚动 DP) |
面试作答时的完整脉络可以概括为三句话:定义"以 i 结尾的最优和"这一状态 → 推出 max(nums[i], dp[i-1]+nums[i]) 的转移并说明其贪心依据(非正前缀必弃)→ 指出滚动优化后 O(n) 时间、O(1) 空间,并用 Integer.MIN_VALUE 初始化正确处理全负边界。这套论证结构直接来自 [notes/42. 连续子数组的最大和.md](https://gitcode.com/GitHub_Trending/cs/CS-Notes/blob/b70121d377cb6005eb65f12b098cd5decd905669/notes/42. 连续子数组的最大和.md?utm_source=gitcode_repo_files) 的参考代码,本文仅在此基础上补齐了推导过程、逐元素验证与复杂度对照,可作为该题解的加深版阅读。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00