作为算法入门的经典题目,最大子数组和(最大子段和)问题是学习时间复杂度优化、分治思想、动态规划的绝佳案例。本文将基于 C++ 实现,从暴力枚举到高效的动态规划,再到拓展的线段树解法,一步步拆解思路、解析代码,带你彻底掌握这个经典问题。
一、问题描述
给定一个整数数组(包含正负值),找到一个连续子数组 (子数组不能为空),使其元素之和最大,并返回这个最大和。示例:数组:{-2,11,-4,13,-5,-2}最大子数组:{11,-4,13},最大和为 20
二、解法一:暴力枚举法(基础思路)
暴力法是最直观的思路:枚举所有可能的子数组,计算和并记录最大值。我实现了两种暴力写法,时间复杂度逐步优化。
1. 三重循环暴力(O (n³))
cpp
#define ArSize(ar)(sizeof(ar)/sizeof(ar[0]))
// 三重循环暴力破解
int MaxSum1(const int* ar, int n)
{
assert(ar != nullptr);
if (n < 1)return 0;
int sum = 0; // 存储最大和
int start = -1, end = -1; // 记录子数组起止下标
// 枚举起点i
for (int i = 0; i < n; ++i)
{
// 枚举终点j
for (int j = i; j < n; ++j)
{
int thissum = 0;
// 累加i~j的元素和
for (int k = i; k <= j; ++k)
{
thissum += ar[k];
}
// 更新最大值
if (thissum > sum)
{
sum = thissum;
start = i;
end = j;
}
}
}
return sum;
}
代码解析:
- 外层两层循环确定子数组的起止位置
[i,j]; - 最内层循环累加
i~j的元素和; - 遍历所有子数组后,得到最大和。缺点 :时间复杂度极高
O(n³),仅适用于极小数组。
2. 二重循环暴力(O (n²))
优化思路:复用上一次的累加结果,去掉最内层循环:
cpp
int MaxSum2(const int* ar, int n)
{
assert(ar != nullptr);
if (n < 1)return 0;
int sum = 0;
int start = -1, end = -1;
for (int i = 0; i < n; ++i)
{
int thissum = 0;
// 直接累加,无需重新遍历
for (int j = i; j < n; ++j)
{
thissum += ar[j];
if (thissum > sum)
{
sum = thissum;
start = i;
end = j;
}
}
}
return sum;
}
优化点 :固定起点i后,终点j逐步右移,直接累加元素,时间复杂度降至O(n²)。
三、解法二:分治策略(O (nlogn))
分治法核心思想:将数组拆分为左右两部分,最大子数组只有三种情况:
- 完全在左半部分;
- 完全在右半部分;
- 跨越左右两部分(包含中点)。
cpp
// 递归分治函数
int MaxSubSum(const int* ar, int left, int right)
{
int sum = 0;
// 递归终止:单个元素
if (left == right)
{
sum = ar[left] > 0 ? ar[left] : 0;
}
else
{
int mid = (right - left) / 2 + left; // 计算中点
// 递归求解左右子问题
int leftsum = MaxSubSum(ar, left, mid);
int rightsum = MaxSubSum(ar, mid + 1, right);
// 计算跨越中点的最大和:左半部分(从mid向左)
int s1 = 0, lefts = 0;
for (int i = mid; i >= left; --i)
{
lefts += ar[i];
if (lefts > s1)s1 = lefts;
}
// 右半部分(从mid+1向右)
int s2 = 0, rights = 0;
for (int i = mid + 1; i <= right; ++i)
{
rights += ar[i];
if (rights > s2)s2 = rights;
}
// 三种情况取最大值
sum = s1 + s2;
sum = max(sum, leftsum);
sum = max(sum, rightsum);
}
return sum;
}
// 分治入口函数
int MaxSum3(const int* ar, int n)
{
assert(ar != nullptr);
if (n < 1)return 0;
return MaxSubSum(ar, 0, n - 1);
}
代码解析:
- 递归拆分数组,直到只剩一个元素;
- 分别计算左、右、跨中点三种情况的最大和;
- 最终返回三者中的最大值。时间复杂度 :
O(nlogn),效率远高于暴力法。
四、解法三:动态规划(最优解 O (n))
动态规划是解决本题的最优算法 ,核心公式:dp[i] = max(0, dp[i-1]) + nums[i]
dp[i]:以第i个元素结尾的最大子数组和;- 如果前一个状态
dp[i-1]为负,直接舍弃,从当前元素重新开始。
1. 数组版 DP(记录中间过程)
cpp
int MaxSum1(const int* ar, int n)
{
assert(ar != nullptr);
if (n < 1)return 0;
if (n == 1)return ar[0] > 0 ? ar[0] : 0;
// 动态分配数组存储中间状态
int* curMaxSum = (int*)calloc(n, sizeof(int));
int* maxSum = (int*)calloc(n, sizeof(int));
// 初始化:第一个元素
curMaxSum[0] = ar[0] > 0 ? ar[0] : 0;
maxSum[0] = curMaxSum[0];
int max = 0;
for (int i = 1; i < n; ++i)
{
// 核心递推公式
curMaxSum[i] = max(0, curMaxSum[i-1]) + ar[i];
// 更新全局最大值
max = max(max, curMaxSum[i]);
maxSum[i] = max;
}
// 释放内存
free(curMaxSum);
free(maxSum);
return max;
}
2. 变量版 DP(空间优化 O (1))
无需数组存储状态,只用两个变量即可完成计算:
cpp
int MaxSum2(const int* ar, int n)
{
assert(ar != nullptr);
if (n < 1)return 0;
if (n == 1)return ar[0] > 0 ? ar[0] : 0;
// 仅用两个变量,空间复杂度O(1)
int curMaxSum = ar[0] > 0 ? ar[0] : 0;
int max = 0;
for (int i = 1; i < n; ++i)
{
curMaxSum = max(0, curMaxSum) + ar[i];
max = max(max, curMaxSum);
}
return max;
}
3. 进阶:记录最大子数组起止位置
在 DP 基础上,增加逻辑记录子数组的起止下标:
cpp
int MaxSum3(const int* ar, int n)
{
assert(ar != nullptr);
if (n < 1)return 0;
if (n == 1)return ar[0] > 0 ? ar[0] : 0;
int curMaxSum = ar[0] > 0 ? ar[0] : 0;
int start = -1, end = -1;
int max = 0;
for (int i = 1; i < n; ++i)
{
curMaxSum = max(0, curMaxSum) + ar[i];
// 重新开始,更新起点
if (curMaxSum == ar[i]) start = i;
// 更新最大值和终点
if (curMaxSum > max)
{
max = curMaxSum;
end = i;
}
}
printf("最大子数组起始下标:%d,终止下标:%d\n", start, end);
return max;
}
测试示例 :数组:{-2,11,40,-13,-5,8}输出:起始下标 1,终止下标 2,最大和51。
五、拓展解法:线段树(区间查询)
线段树适用于多次区间查询的场景,本文实现了基础的区间和查询线段树:
cpp
class SegmentTree {
private:
vector<int>tree; // 线段树数组
int n; // 原数组长度
// 构建线段树
void build(const vector<int>& ar, int node, int start, int end)
{
if (start == end)
{
tree[node] = ar[start];
return;
}
int mid = (start + end) / 2;
int left = 2*node+1, right = 2*node+2;
build(ar, left, start, mid);
build(ar, right, mid+1, end);
tree[node] = tree[left] + tree[right];
}
// 区间查询
int query(int node, int start, int end, int l, int r)
{
if (r<start || l>end) return 0;
if (l<=start && end<=r) return tree[node];
int mid = (start+end)/2;
int left = 2*node+1, right = 2*node+2;
return query(left,start,mid,l,r) + query(right,mid+1,end,l,r);
}
public:
SegmentTree(const vector<int>& ar)
{
n = ar.size();
tree.resize(4*n, 0);
build(ar, 0, 0, n-1);
}
// 对外接口:查询[l,r]区间和
int rangeQuery(int l, int r)
{
return query(0,0,n-1,l,r);
}
};
图解:

适用场景 :需要频繁查询数组任意区间和的场景,单次查询O(logn)。
六、测试主函数
cpp
int main()
{
int ar[] = {-2,11,-4,13,-5,-2};
int n = ArSize(ar);
// 测试分治法
cout << "分治法最大子数组和:" << MaxSum3(ar, n) << endl;
// 测试动态规划
cout << "动态规划最大子数组和:" << MaxSum2(ar, n) << endl;
return 0;
}
运行结果 :20
七、四种算法对比
表格
| 算法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 三重暴力 | O(n³) | O(1) | 学习入门、极小数组 |
| 二重暴力 | O(n²) | O(1) | 简单场景、小数组 |
| 分治策略 | O(nlogn) | O(logn) | 理解分治思想 |
| 动态规划 | O(n) | O(1) | 最优解、通用场景 |
| 线段树 | O (n) 构建 | O(4n) | 多次区间查询 |
八、总结
- 最大子数组和是动态规划的经典入门题 ,最优解时间复杂度
O(n)、空间复杂度O(1); - 暴力法直观但效率低,分治法体现递归拆分思想,动态规划是工业级最优解;
- 核心公式:
curMaxSum = max(0, curMaxSum) + nums[i],舍弃负收益的前缀。
本文完整实现了所有经典解法,代码可直接运行,适合算法初学者学习~有问题欢迎评论区交流!