📌 题目链接:leetcode.cn/problems/ma…
🔍 难度:中等 | 🏷️ 标签:数组、动态规划、分治
⏱️ 目标时间复杂度:O(n)
💾 空间复杂度:O(1)(动态规划解法)
题目分析
给定一个整数数组 nums,找出一个连续子数组(至少包含一个元素),使得其元素和最大,返回这个最大和。
这是一个经典的「最优化」问题,要求在所有可能的连续子数组中找到和最大的那个。由于子数组必须是连续的,因此不能跳着选元素,也不能重新排序。
示例回顾:
- 输入:
[-2,1,-3,4,-1,2,1,-5,4]→ 输出:6(对应子数组[4,-1,2,1]) - 输入:
[1]→ 输出:1 - 输入:
[5,4,-1,7,8]→ 输出:23
💡 关键点:负数会“拖累”总和,但有时为了连接后面的正数,不得不暂时接受负数。如何权衡?——这就是动态规划要解决的问题。
核心算法及代码讲解
本题有两种主流解法:动态规划(推荐) 和 分治法(进阶)。面试中最常考察的是动态规划,因其简洁高效;而分治法则体现了结构化思维,适用于扩展场景(如区间查询、支持修改等)。
✅ 方法一:动态规划(Kadane 算法)
这是由 Jay Kadane 提出的经典算法,专用于解决最大子数组和问题。
🧠 核心思想:
定义状态 f(i) 表示以第 i 个元素结尾的连续子数组的最大和。
那么状态转移方程为:
f(i) = max(f(i-1) + nums[i], nums[i])
解释:
- 如果前面的子数组和
f(i-1)是正的,加上当前元素nums[i]会让总和更大; - 如果
f(i-1)是负的,不如从nums[i]重新开始一段新子数组。
最终答案就是所有 f(i) 中的最大值。
🚀 优化空间:
由于 f(i) 只依赖 f(i-1),我们不需要开数组,只需用一个变量 pre 记录前一个状态即可,实现 O(1) 空间。
🔍 代码详解(含行注释):
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int pre = 0; // pre 表示 f(i-1),即以上一个元素结尾的最大子数组和
int maxAns = nums[0]; // 初始化答案为第一个元素(防止全负)
for (const auto &x : nums) {
pre = max(pre + x, x); // 状态转移:要么接上前面,要么从 x 重新开始
maxAns = max(maxAns, pre); // 实时更新全局最大值
}
return maxAns;
}
};
✅ 为什么初始化
maxAns = nums[0]?
因为题目保证数组非空,且若全为负数(如[-3,-1,-2]),最大子数组就是最大的那个负数,不能初始化为 0!
🌲 方法二:分治法(线段树思想雏形)
虽然动态规划更优,但分治法展示了如何将问题分解并合并结果,是理解线段树、区间查询类问题的基础。
🧠 核心思想:
对区间 [l, r],递归处理左右两半,然后合并信息。我们需要维护四个值:
| 字段 | 含义 |
|---|---|
iSum | 区间总和 |
lSum | 以左端点开头的最大子数组和 |
rSum | 以右端点结尾的最大子数组和 |
mSum | 区间内任意位置的最大子数组和(即所求) |
🔁 合并规则(pushUp):
假设左子区间为 L,右子区间为 R:
iSum = L.iSum + R.iSumlSum = max(L.lSum, L.iSum + R.lSum)rSum = max(R.rSum, R.iSum + L.rSum)mSum = max(L.mSum, R.mSum, L.rSum + R.lSum)
💡 跨越中点的情况:最大子数组可能横跨左右,此时和为
L.rSum + R.lSum。
🔍 代码详解(含行注释):
struct Status {
int lSum, rSum, mSum, iSum;
};
Status pushUp(Status l, Status r) {
int iSum = l.iSum + r.iSum;
int lSum = max(l.lSum, l.iSum + r.lSum); // 要么只取左边,要么左边全取+右边前缀
int rSum = max(r.rSum, r.iSum + l.rSum); // 要么只取右边,要么右边全取+左边后缀
int mSum = max({l.mSum, r.mSum, l.rSum + r.lSum}); // 三种情况取最大
return {lSum, rSum, mSum, iSum};
}
Status get(vector<int> &a, int l, int r) {
if (l == r) {
return {a[l], a[l], a[l], a[l]}; // 叶子节点:四个值都等于 a[l]
}
int m = (l + r) >> 1;
Status lSub = get(a, l, m);
Status rSub = get(a, m + 1, r);
return pushUp(lSub, rSub);
}
int maxSubArray(vector<int>& nums) {
return get(nums, 0, nums.size() - 1).mSum;
}
⚠️ 注意:C++11 以后
max({a,b,c})需要<algorithm>,但<bits/stdc++.h>已包含。
解题思路(分步骤)
动态规划解法步骤:
- 初始化:
pre = 0(前一个子数组和),maxAns = nums[0](防止全负)。 - 遍历数组:对每个元素
x:- 更新
pre = max(pre + x, x):决定是否延续前面的子数组。 - 更新
maxAns = max(maxAns, pre):记录历史最大值。
- 更新
- 返回结果:
maxAns即为答案。
分治法步骤:
- 递归划分:将数组不断二分,直到单个元素。
- 回溯合并:从叶子向上合并,计算每个区间的四个关键值。
- 返回根节点的
mSum:即整个数组的最大子数组和。
算法分析
| 方法 | 时间复杂度 | 空间复杂度 | 是否推荐 | 面试价值 |
|---|---|---|---|---|
| 动态规划 | O(n) | O(1) | ✅ 强烈推荐 | ⭐⭐⭐⭐⭐ |
| 分治法 | O(n) | O(log n) | ❌ 了解即可 | ⭐⭐⭐(展示思维深度) |
💬 面试官可能会问:
- “如果数组全是负数怎么办?” → 动态规划天然支持,因为每次
max(pre+x, x)会保留最大负数。- “能处理空数组吗?” → 题目保证非空,但实际工程中需判空。
- “这个算法叫什么?” → Kadane’s Algorithm,务必记住名字!
完整代码(保留原模板,含测试)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
// ==================== 动态规划解法(主推) ====================
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int pre = 0, maxAns = nums[0];
for (const auto &x: nums) {
pre = max(pre + x, x);
maxAns = max(maxAns, pre);
}
return maxAns;
}
};
// ==================== 分治法(进阶) ====================
struct Status {
int lSum, rSum, mSum, iSum;
};
Status pushUp(Status l, Status r) {
int iSum = l.iSum + r.iSum;
int lSum = max(l.lSum, l.iSum + r.lSum);
int rSum = max(r.rSum, r.iSum + l.rSum);
int mSum = max({l.mSum, r.mSum, l.rSum + r.lSum});
return {lSum, rSum, mSum, iSum};
}
Status get(vector<int> &a, int l, int r) {
if (l == r) {
return {a[l], a[l], a[l], a[l]};
}
int m = (l + r) >> 1;
Status lSub = get(a, l, m);
Status rSub = get(a, m + 1, r);
return pushUp(lSub, rSub);
}
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
Solution sol;
vector<int> nums1 = {-2,1,-3,4,-1,2,1,-5,4};
cout << "Test 1: " << sol.maxSubArray(nums1) << " (Expected: 6)" << endl;
vector<int> nums2 = {1};
cout << "Test 2: " << sol.maxSubArray(nums2) << " (Expected: 1)" << endl;
vector<int> nums3 = {5,4,-1,7,8};
cout << "Test 3: " << sol.maxSubArray(nums3) << " (Expected: 23)" << endl;
// 分治法测试(可选)
cout << "Divide & Conquer Test: " << get(nums1, 0, nums1.size()-1).mSum << endl;
return 0;
}
💡 面试高频考点总结
- Kadane 算法 是最大子数组和的标准解法,必须手写无误。
- 状态定义 是动态规划的关键:
f(i)表示“以 i 结尾”的最大和,而非“到 i 为止”的最大和。 - 边界处理:全负数组的正确性验证。
- 空间优化:滚动变量替代数组,体现工程素养。
- 扩展问题:
- 返回子数组起止位置?
- 求最大子数组乘积?(需同时记录最大最小值)
- 支持修改数组元素后的多次查询?→ 引入线段树
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📣 下一期预告:LeetCode 热题 100 第 56 题 —— 合并区间(中等)
🔹 题目:给你一个二维整数数组
intervals,其中intervals[i] = [start_i, end_i]表示第 i 个区间的开始和结束。请你合并所有重叠的区间,并返回一个不重叠的区间列表,该列表需按起始点升序排列。🔹 核心思路:先按起点排序,然后遍历合并相邻或重叠的区间。
🔹 考点:排序、贪心、区间合并、边界判断。
🔹 难度:中等,但非常常见于系统设计与调度问题中,是区间类问题的入门经典。
💡 提示:不要暴力枚举所有区间组合!排序 + 贪心是关键!
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!