一、前置知识
在学这道题之前,我们先搞懂所有会用到的基础概念,不用怕,全是大白话,没有复杂术语,每一个点都讲透,确保你能跟上后续所有内容。
1. 核心概念:数组、整数数组、连续子数组
① 数组:简单说就是"把多个数据排成一排,按顺序存放",比如班级里的同学按座位排成一排,每个同学就是一个"元素",每个元素都有自己的"位置"(专业叫"索引")。
② 整数数组:数组里的每个元素都是整数(可以是正数,比如5、8;负数,比如-2、-5;0),这道题的输入nums就是"整数数组"。
③ 连续子数组(重点!这道题的核心要求):子数组就是"从数组里挑一段",但必须满足"连续"------不能跳着挑元素。比如数组[1,2,3,4],连续子数组可以是[1](只挑第一个)、[1,2](前两个)、[2,3,4](后三个),但不能是[1,3](跳了2)、[2,4](跳了3)。
补充:子数组最少包含1个元素(哪怕数组全是负数,也要挑最大的那个负数,比如数组[-5,-3,-2],最大子数组就是[-2],和为-2)。
2. 基础语法:变量、循环、条件判断
① 变量:用来"存数据"的容器,比如我们要存"最大和",就定义一个变量max_sum,把计算出来的和存进去,后续可以随时修改、调用。
② for循环:用来"重复做一件事",比如我们要遍历数组的每一个元素,就用for循环,从第一个元素(索引0)依次走到最后一个元素(索引n-1,n是数组长度)。
举个大白话例子:遍历数组[1,2,3],循环就是"先拿1,再拿2,最后拿3",重复3次(数组长度是3)。
③ 条件判断(if语句):用来"做选择",比如我们计算出一个子数组的和,要判断这个和是不是比当前的"最大和"大,如果是,就更新最大和。
3. 关键工具:常用常量、函数
① C++里的常用内容:
-
#include <vector>:引入"向量"(也就是C++里的数组,和我们说的数组完全一样,只是写法不同)的头文件,没有这句话,就不能用vector定义数组。
-
#include <climits>:引入"极限值"头文件,里面有INT_MIN(表示C++里能存的最小整数,大概是-2147483648),用来处理"数组全是负数"的情况(比如初始时把最大和设为INT_MIN,避免漏了负数)。
-
#include <algorithm>:引入"算法"头文件,里面有max()函数,用来比较两个数的大小,比如max(3,5),结果就是5。
-
using namespace std;:简化写法,比如原本要写std::vector,加了这句话后,直接写vector就行,不用多写std::。
-
vector<int>& nums:表示"传入一个整数数组nums",&符号表示"引用传递"(不用深懂,记住:这样写能节省内存,而且能直接操作原数组,不用复制一份)。
② Python里的常用内容:
-
from typing import List:用来指定函数参数的类型,List表示"列表"(也就是Python里的数组),nums: List[int]表示nums是一个整数列表。
-
float('-inf'):表示Python里的"负无穷大",和C++的INT_MIN作用一样,用来处理全负数数组(初始时最大和设为负无穷,不管遇到什么负数,都能更新成这个负数)。
-
len(nums):获取数组的长度,比如nums = [1,2,3],len(nums)就是3。
4. 算法基础:时间复杂度、空间复杂度
① 时间复杂度:表示"算法要做多少件事",值越小,算法越快。
-
O(n²):两层for循环,比如n=100,就要做100×100=10000件事,速度较慢(暴力法用这个)。
-
O(n):一层for循环,n=100,只做100件事,速度很快(动态规划、Kadane算法用这个)。
-
O(logn):递归或二分法用到,比如n=100,大概只做7件事(分治法用这个)。
② 空间复杂度:表示"算法需要占用多少内存",值越小,占用内存越少。
-
O(1):只用到几个变量(比如max_sum、current_sum),不占用额外内存(Kadane算法用这个)。
-
O(n):需要一个和原数组一样长的数组(比如动态规划的dp数组),占用内存中等。
-
O(logn):递归时用到"递归栈",占用内存较少(分治法用这个)。
5. 进阶基础:动态规划、递归
① 动态规划(DP):核心思想是"把大问题拆成小问题,用小问题的答案推大问题的答案"。
举个大白话例子:想求"以第5个元素结尾的最大子数组和",不用重新计算所有包含第5个元素的子数组,只需要看"以第4个元素结尾的最大子数组和"------如果第4个结尾的和是正的,就把第5个元素加上;如果是负的,就只取第5个元素(加了反而更小)。
② 递归:核心思想是"自己调用自己,把大区间拆成小区间,解决小区间后,合并结果"。
举个大白话例子:想求数组[1,2,3,4]的最大子数组和,先拆成左半[1,2]和右半[3,4],分别求左半、右半的最大和,再求"跨越中间元素(2和3)"的最大和,三者取最大就是整个数组的最大和。
二、题目解析
题目:给你一个整数数组nums,请你找出一个"连续子数组"(最少包含1个元素),这个子数组的和是所有连续子数组里最大的,最后返回这个最大和。
核心3个要求,再强调一遍:
-
连续:子数组不能跳元素,必须是数组中一段连续的片段(比如[4,-1,2,1]是连续的,[4,2,1]就不连续)。
-
最少1个元素:哪怕数组全是负数,也要选最大的那个负数(比如数组[-5,-3,-2],最大子数组是[-2],和为-2)。
-
返回值:不是返回最大子数组本身,而是返回这个子数组的"和"(比如示例1的输出是6,不是[4,-1,2,1])。
示例补充(逐句看):
示例1:输入nums = [-2,1,-3,4,-1,2,1,-5,4]
所有连续子数组的和里,[4,-1,2,1]的和是4+(-1)+2+1=6,是最大的,所以输出6。
示例2:输入nums = [1],只有1个元素,子数组只能是[1],和为1,输出1。
示例3:输入nums = [5,4,-1,7,8],最大子数组是整个数组,和为5+4+(-1)+7+8=23,输出23。
三、解法讲解(由易到难,暴力→动态规划→Kadane→分治,每步讲透)
所有解法都包含:思路(大白话)→ 代码(逐句注释)→ 调用流程→ 运行过程(结合示例一步步算)→ 主函数测试(含示例+自定义输入),确保能跟着走,能自己运行代码。
解法1:暴力枚举法(最直观,入门首选,能直接看懂)
1. 核心思路(大白话)
既然要找"最大和的连续子数组",那我们就把"所有可能的连续子数组"都找出来,计算每个子数组的和,然后记录下最大的那个和------这就是暴力法的核心,简单、直观,就是有点慢,但能帮我们理解题目本质。
具体步骤:
① 遍历所有"起始位置i":i表示子数组从哪个元素开始(比如i=0,就是从第一个元素开始;i=1,就是从第二个元素开始)。
② 对每个起始位置i,遍历所有"结束位置j":j必须≥i(因为子数组要连续,结束位置不能在起始位置前面),j表示子数组到哪个元素结束。
③ 累加i到j的所有元素,得到当前子数组的和,然后和"当前最大和"比较,如果更大,就更新最大和。
④ 遍历完所有子数组后,返回最大和。
2. C++代码(逐句注释,每句都讲含义)
cpp
#include <vector> // 引入向量(C++的数组)头文件,没有这句话就不能用vector
#include <climits> // 引入极限值头文件,用来使用INT_MIN(最小整数)
#include <algorithm>// 引入算法头文件,用来使用max()函数(比较两个数的大小)
using namespace std; // 简化写法,后续不用写std::vector,直接写vector
// 定义Solution类,LeetCode题目固定格式,里面放解题函数
class Solution {
public:
// 定义解题函数maxSubArray,参数是整数数组nums(引用传递),返回值是int类型(最大和)
int maxSubArray(vector<int>& nums) {
// 1. 计算数组的长度n,nums.size()就是数组的元素个数
int n = nums.size();
// 2. 初始化最大和max_sum为INT_MIN(最小整数),防止数组全是负数时,漏了最小的负数
int max_sum = INT_MIN;
// 3. 第一层for循环:枚举所有起始位置i(从0到n-1,因为数组索引从0开始)
for (int i = 0; i < n; i++) {
// 4. 定义current_sum(当前子数组的和),初始化为0,每次换起始位置i,都要重置为0
int current_sum = 0;
// 5. 第二层for循环:枚举所有结束位置j(从i到n-1,j≥i,保证子数组连续)
for (int j = i; j < n; j++) {
// 6. 累加当前元素nums[j]到current_sum,得到i到j的子数组和
current_sum += nums[j];
// 7. 条件判断:如果当前子数组和current_sum比max_sum大,就更新max_sum
if (current_sum > max_sum) {
max_sum = current_sum;
}
}
}
// 8. 遍历完所有子数组,返回最大和max_sum
return max_sum;
}
};
// 主函数(测试函数):用来运行代码,测试示例和自定义输入,可以直接复制运行
int main() {
// 定义Solution对象sol,用来调用maxSubArray函数
Solution sol;
// 测试示例1:输入nums = [-2,1,-3,4,-1,2,1,-5,4],预期输出6
vector<int> nums1 = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
int result1 = sol.maxSubArray(nums1); // 调用解题函数,传入nums1,得到结果
cout << "示例1输出:" << result1 << endl; // 打印结果,应该输出6
// 测试示例2:输入nums = [1],预期输出1
vector<int> nums2 = {1};
int result2 = sol.maxSubArray(nums2);
cout << "示例2输出:" << result2 << endl; // 打印结果,应该输出1
// 测试示例3:输入nums = [5,4,-1,7,8],预期输出23
vector<int> nums3 = {5, 4, -1, 7, 8};
int result3 = sol.maxSubArray(nums3);
cout << "示例3输出:" << result3 << endl; // 打印结果,应该输出23
// 自定义测试1:全负数数组,输入nums = [-5,-3,-2,-7],预期输出-2
vector<int> nums4 = {-5, -3, -2, -7};
int result4 = sol.maxSubArray(nums4);
cout << "自定义测试1(全负数)输出:" << result4 << endl; // 输出-2
// 自定义测试2:全正数数组,输入nums = [1,2,3,4],预期输出10
vector<int> nums5 = {1, 2, 3, 4};
int result5 = sol.maxSubArray(nums5);
cout << "自定义测试2(全正数)输出:" << result5 << endl; // 输出10
return 0; // 主函数结束
}
3. Python代码(逐句注释,每句都讲含义)
python
from typing import List # 引入List类型,用来指定函数参数是整数列表(数组)
# 定义Solution类,LeetCode题目固定格式
class Solution:
# 定义解题函数maxSubArray,参数nums是整数列表,返回值是int类型(最大和)
def maxSubArray(self, nums: List[int]) -> int:
# 1. 计算数组的长度n,len(nums)就是数组的元素个数
n = len(nums)
# 2. 初始化最大和max_sum为负无穷(float('-inf')),处理全负数数组
max_sum = float('-inf')
# 3. 第一层for循环:枚举所有起始位置i(range(n)表示0到n-1,数组索引从0开始)
for i in range(n):
# 4. 定义current_sum(当前子数组的和),初始化为0,换起始位置i就重置
current_sum = 0
# 5. 第二层for循环:枚举所有结束位置j(从i到n-1,j≥i,保证连续)
for j in range(i, n):
# 6. 累加当前元素nums[j]到current_sum,得到i到j的子数组和
current_sum += nums[j]
# 7. 条件判断:如果当前和比max_sum大,更新max_sum
if current_sum > max_sum:
max_sum = current_sum
# 8. 遍历完所有子数组,返回最大和
return max_sum
# 主函数(测试函数):运行代码,测试示例和自定义输入,可直接复制运行
if __name__ == "__main__":
# 实例化Solution对象sol,用来调用maxSubArray函数
sol = Solution()
# 测试示例1:输入nums = [-2,1,-3,4,-1,2,1,-5,4],预期输出6
nums1 = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
result1 = sol.maxSubArray(nums1) # 调用函数,传入nums1,获取结果
print("示例1输出:", result1) # 打印结果,输出6
# 测试示例2:输入nums = [1],预期输出1
nums2 = [1]
result2 = sol.maxSubArray(nums2)
print("示例2输出:", result2) # 输出1
# 测试示例3:输入nums = [5,4,-1,7,8],预期输出23
nums3 = [5, 4, -1, 7, 8]
result3 = sol.maxSubArray(nums3)
print("示例3输出:", result3) # 输出23
# 自定义测试1:全负数数组,输入nums = [-5,-3,-2,-7],预期输出-2
nums4 = [-5, -3, -2, -7]
result4 = sol.maxSubArray(nums4)
print("自定义测试1(全负数)输出:", result4) # 输出-2
# 自定义测试2:全正数数组,输入nums = [1,2,3,4],预期输出10
nums5 = [1, 2, 3, 4]
result5 = sol.maxSubArray(nums5)
print("自定义测试2(全正数)输出:", result5) # 输出10
4. 调用流程(能看懂的执行步骤)
以C++为例(Python流程完全一样,只是语法不同):
① 运行主函数main(),先定义Solution对象sol(用来调用解题函数)。
② 定义测试数组(比如nums1),然后调用sol.maxSubArray(nums1),进入解题函数。
③ 解题函数中,先计算数组长度n,初始化max_sum为INT_MIN,current_sum为0。
④ 执行第一层for循环(i从0到n-1),每次i变化,current_sum重置为0。
⑤ 执行第二层for循环(j从i到n-1),累加nums[j]到current_sum,每次累加后判断是否更新max_sum。
⑥ 所有循环结束,解题函数返回max_sum,主函数接收结果,打印输出。
5. 运行过程(结合示例1,一步步算,跟着算一遍就懂)
示例1输入:nums = [-2,1,-3,4,-1,2,1,-5,4],n=9。
① i=0(起始位置是第一个元素-2):
j=0:current_sum = -2 → max_sum从INT_MIN变成-2;
j=1:current_sum = -2+1 = -1 → max_sum变成-1;
j=2:current_sum = -1 + (-3) = -4 → 比-1小,不更新;
j=3:current_sum = -4 +4 =0 → 比-1大,max_sum变成0;
j=4:current_sum=0+(-1)=-1 → 不更新;
j=5:current_sum=-1+2=1 → max_sum变成1;
j=6:current_sum=1+1=2 → max_sum变成2;
j=7:current_sum=2+(-5)=-3 → 不更新;
j=8:current_sum=-3+4=1 → 不更新;
② i=1(起始位置是1):
j=1:current_sum=1 → max_sum变成1(和当前一样,不更新);
j=2:1+(-3)=-2 → 不更新;
j=3:-2+4=2 → max_sum变成2;
... 后续j继续遍历,current_sum最大到2,不超过之前的2;
③ i=3(起始位置是4,重点!):
j=3:current_sum=4 → max_sum变成4;
j=4:4+(-1)=3 → 不更新;
j=5:3+2=5 → max_sum变成5;
j=6:5+1=6 → max_sum变成6;
j=7:6+(-5)=1 → 不更新;
j=8:1+4=5 → 不更新;
④ 后续i=4、5、6、7、8,遍历后current_sum都不会超过6,所以最终max_sum=6,输出6。
6. 复杂度总结
时间复杂度:O(n²)(两层for循环,n是数组长度),比如n=1000,就要执行1000×1000=100万次,速度较慢;
空间复杂度:O(1)(只用到了max_sum、current_sum、n这3个变量,不占用额外内存)。
解法2:动态规划法(优化时间,入门DP,能懂的思路)
1. 核心思路(大白话,结合前置知识)
暴力法太慢,因为做了很多重复计算(比如计算i=0、j=3的和时,已经算了i=0、j=2的和,暴力法会重新算一遍)。动态规划就是"避免重复计算",把大问题拆成小问题。
具体思路(重点,逐句懂):
① 定义"状态":dp[i] 表示"以第i个元素结尾的最大子数组和"(i是数组索引,从0开始)。
解释:dp[i]只关注"结尾是第i个元素"的子数组,不管前面的元素,这样就能把"整个数组的最大子数组和"拆成"每个位置结尾的最大子数组和",最后取这些和的最大值即可。
② 状态转移方程(核心中的核心):
-
如果 dp[i-1] > 0:说明"以第i-1个元素结尾的最大子数组和"是正的,把第i个元素加上,和会更大 → dp[i] = dp[i-1] + nums[i];
-
如果 dp[i-1] ≤ 0:说明"以第i-1个元素结尾的最大子数组和"是负的(或0),加上第i个元素只会更小,不如直接取第i个元素本身 → dp[i] = nums[i];
③ 初始条件:dp[0] = nums[0](因为第0个元素结尾的子数组,只能是它自己,和就是它本身)。
④ 最终结果:dp数组中所有元素的最大值(因为最大子数组可能以任意一个元素结尾)。
2. C++代码(逐句注释,每句讲含义)
cpp
#include <vector>
#include <algorithm> // 用到max()函数,比较两个数的大小
using namespace std;
class Solution {
public:
int maxSubArray(vector<int>& nums) {
// 1. 计算数组长度n
int n = nums.size();
// 2. 定义dp数组,dp[i]表示以第i个元素结尾的最大子数组和,长度和nums一样
vector<int> dp(n);
// 3. 初始条件:第0个元素结尾的子数组,和就是它自己
dp[0] = nums[0];
// 4. 初始化最大和max_sum,初始值是dp[0](因为至少有一个元素)
int max_sum = dp[0];
// 5. 遍历数组,从第1个元素开始(i=1,因为i=0已经初始化)
for (int i = 1; i < n; i++) {
// 6. 核心:状态转移方程,判断dp[i-1]是否为正,决定dp[i]的值
// max(nums[i], dp[i-1] + nums[i]):取"自己本身"和"自己+前面的最大和"中较大的那个
dp[i] = max(nums[i], dp[i-1] + nums[i]);
// 7. 更新全局最大和:如果当前dp[i]比max_sum大,就更新
max_sum = max(max_sum, dp[i]);
}
// 8. 返回全局最大和
return max_sum;
}
};
// 主函数(测试函数,和暴力法一样,测试示例+自定义输入)
int main() {
Solution sol;
// 测试示例1:预期6
vector<int> nums1 = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
cout << "示例1输出:" << sol.maxSubArray(nums1) << endl;
// 测试示例2:预期1
vector<int> nums2 = {1};
cout << "示例2输出:" << sol.maxSubArray(nums2) << endl;
// 测试示例3:预期23
vector<int> nums3 = {5, 4, -1, 7, 8};
cout << "示例3输出:" << sol.maxSubArray(nums3) << endl;
// 自定义测试1:全负数,预期-2
vector<int> nums4 = {-5, -3, -2, -7};
cout << "自定义测试1输出:" << sol.maxSubArray(nums4) << endl;
// 自定义测试2:全正数,预期10
vector<int> nums5 = {1, 2, 3, 4};
cout << "自定义测试2输出:" << sol.maxSubArray(nums5) << endl;
return 0;
}
3. Python代码(逐句注释,每句讲含义)
python
from typing import List
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
# 1. 计算数组长度n
n = len(nums)
# 2. 定义dp数组,长度和nums一样,初始值全为0
dp = [0] * n
# 3. 初始条件:第0个元素结尾的子数组和,就是nums[0]
dp[0] = nums[0]
# 4. 初始化最大和max_sum,初始值为dp[0]
max_sum = dp[0]
# 5. 遍历数组,从第1个元素开始(i=1)
for i in range(1, n):
# 6. 状态转移方程:取"自己本身"和"自己+前面的最大和"中较大的
dp[i] = max(nums[i], dp[i-1] + nums[i])
# 7. 更新全局最大和
max_sum = max(max_sum, dp[i])
# 8. 返回最大和
return max_sum
# 主函数(测试函数)
if __name__ == "__main__":
sol = Solution()
# 测试示例1:预期6
nums1 = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
print("示例1输出:", sol.maxSubArray(nums1))
# 测试示例2:预期1
nums2 = [1]
print("示例2输出:", sol.maxSubArray(nums2))
# 测试示例3:预期23
nums3 = [5, 4, -1, 7, 8]
print("示例3输出:", sol.maxSubArray(nums3))
# 自定义测试1:全负数,预期-2
nums4 = [-5, -3, -2, -7]
print("自定义测试1输出:", sol.maxSubArray(nums4))
# 自定义测试2:全正数,预期10
nums5 = [1, 2, 3, 4]
print("自定义测试2输出:", sol.maxSubArray(nums5))
4. 调用流程(和暴力法类似,简化了循环)
① 主函数定义测试数组,调用sol.maxSubArray(nums),进入解题函数。
② 解题函数中,计算n,定义dp数组,初始化dp[0]和max_sum。
③ 一层for循环(i从1到n-1),每次计算dp[i](用状态转移方程),然后更新max_sum。
④ 循环结束,返回max_sum,主函数打印结果。
5. 运行过程(结合示例1,一步步算dp数组,跟着算)
示例1输入:nums = [-2,1,-3,4,-1,2,1,-5,4],n=9。
① 初始化:dp[0] = -2,max_sum = -2。
② i=1(nums[1]=1):
dp[0] = -2 ≤ 0 → dp[1] = nums[1] = 1;max_sum = max(-2, 1) = 1。
③ i=2(nums[2]=-3):
dp[1] = 1 > 0 → dp[2] = 1 + (-3) = -2;max_sum = max(1, -2) = 1。
④ i=3(nums[3]=4):
dp[2] = -2 ≤ 0 → dp[3] = 4;max_sum = max(1, 4) = 4。
⑤ i=4(nums[4]=-1):
dp[3] = 4 > 0 → dp[4] = 4 + (-1) = 3;max_sum = max(4, 3) = 4。
⑥ i=5(nums[5]=2):
dp[4] = 3 > 0 → dp[5] = 3 + 2 = 5;max_sum = max(4, 5) = 5。
⑦ i=6(nums[6]=1):
dp[5] = 5 > 0 → dp[6] = 5 + 1 = 6;max_sum = max(5, 6) = 6。
⑧ i=7(nums[7]=-5):
dp[6] = 6 > 0 → dp[7] = 6 + (-5) = 1;max_sum = max(6, 1) = 6。
⑨ i=8(nums[8]=4):
dp[7] = 1 > 0 → dp[8] = 1 + 4 = 5;max_sum = max(6, 5) = 6。
最终dp数组:[-2,1,-2,4,3,5,6,1,5],最大值是6,输出6。
6. 复杂度总结
时间复杂度:O(n)(只有一层for循环,n是数组长度),比暴力法快很多;
空间复杂度:O(n)(需要一个dp数组,长度和原数组一样)。
解法3:Kadane算法(最优解,面试首选,空间优化版动态规划)
1. 核心思路(大白话,优化动态规划的空间)
观察动态规划法,我们发现:dp[i] 只依赖于 dp[i-1](计算dp[i]时,只需要知道dp[i-1]的值,不需要整个dp数组)。
所以,我们可以用"一个变量"代替"整个dp数组",把空间复杂度从O(n)优化到O(1),这就是Kadane算法------本质是"空间优化后的动态规划",思路和动态规划完全一样,只是更省内存。
具体思路:
① 用 current_max 代替 dp[i]:current_max 表示"以当前元素结尾的最大子数组和"(和dp[i]含义一样)。
② 用 max_sum 记录全局最大和(和动态规划一样)。
③ 初始化:current_max 和 max_sum 都等于 nums[0](因为第一个元素结尾的子数组和就是它自己)。
④ 遍历数组(从第二个元素开始):
-
current_max = max(nums[i], current_max + nums[i])(和动态规划的状态转移方程一样);
-
max_sum = max(max_sum, current_max)(更新全局最大和)。
⑤ 最终返回 max_sum。
2. C++代码(逐句注释,每句讲含义)
cpp
#include <vector>
#include <algorithm> // 用到max()函数
using namespace std;
class Solution {
public:
int maxSubArray(vector<int>& nums) {
// 1. 初始化current_max(当前结尾的最大和)和max_sum(全局最大和)
// 都等于nums[0],因为第一个元素结尾的子数组和就是它自己
int current_max = nums[0];
int max_sum = nums[0];
// 2. 计算数组长度n
int n = nums.size();
// 3. 遍历数组,从第二个元素开始(i=1)
for (int i = 1; i < n; i++) {
// 4. 核心:和动态规划的状态转移方程一样,判断current_max是否为正
current_max = max(nums[i], current_max + nums[i]);
// 5. 更新全局最大和
max_sum = max(max_sum, current_max);
}
// 6. 返回全局最大和
return max_sum;
}
};
// 主函数(测试函数,和前面一致)
int main() {
Solution sol;
vector<int> nums1 = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
cout << "示例1输出:" << sol.maxSubArray(nums1) << endl; // 6
vector<int> nums2 = {1};
cout << "示例2输出:" << sol.maxSubArray(nums2) << endl; // 1
vector<int> nums3 = {5, 4, -1, 7, 8};
cout << "示例3输出:" << sol.maxSubArray(nums3) << endl; // 23
vector<int> nums4 = {-5, -3, -2, -7};
cout << "自定义测试1输出:" << sol.maxSubArray(nums4) << endl; // -2
vector<int> nums5 = {1, 2, 3, 4};
cout << "自定义测试2输出:" << sol.maxSubArray(nums5) << endl; // 10
return 0;
}
3. Python代码(逐句注释,每句讲含义)
python
from typing import List
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
# 1. 初始化current_max和max_sum,都等于nums[0]
current_max = max_sum = nums[0]
# 2. 遍历数组,从第二个元素开始(nums[1:]表示从索引1到最后)
for num in nums[1:]:
# 3. 核心:状态转移,判断current_max是否为正
current_max = max(num, current_max + num)
# 4. 更新全局最大和
max_sum = max(max_sum, current_max)
# 5. 返回全局最大和
return max_sum
# 主函数(测试函数)
if __name__ == "__main__":
sol = Solution()
nums1 = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
print("示例1输出:", sol.maxSubArray(nums1)) # 6
nums2 = [1]
print("示例2输出:", sol.maxSubArray(nums2)) # 1
nums3 = [5, 4, -1, 7, 8]
print("示例3输出:", sol.maxSubArray(nums3)) # 23
nums4 = [-5, -3, -2, -7]
print("自定义测试1输出:", sol.maxSubArray(nums4)) # -2
nums5 = [1, 2, 3, 4]
print("自定义测试2输出:", sol.maxSubArray(nums5)) # 10
4. 调用流程(和动态规划几乎一样,更简洁)
① 主函数调用sol.maxSubArray(nums),进入解题函数。
② 初始化current_max和max_sum为nums[0]。
③ 一层for循环,遍历从第二个元素开始的所有元素,每次更新current_max和max_sum。
④ 循环结束,返回max_sum,主函数打印结果。
5. 运行过程(结合示例1,和动态规划一致,只是用变量代替数组)
示例1输入:nums = [-2,1,-3,4,-1,2,1,-5,4]。
① 初始化:current_max = -2,max_sum = -2。
② 遍历num=1(第二个元素):
current_max = max(1, -2+1)=1;max_sum = max(-2,1)=1。
③ 遍历num=-3(第三个元素):
current_max = max(-3, 1+(-3))=-2;max_sum = max(1,-2)=1。
④ 遍历num=4(第四个元素):
current_max = max(4, -2+4)=4;max_sum = max(1,4)=4。
⑤ 遍历num=-1(第五个元素):
current_max = max(-1,4+(-1))=3;max_sum = max(4,3)=4。
⑥ 遍历num=2(第六个元素):
current_max = max(2,3+2)=5;max_sum = max(4,5)=5。
⑦ 遍历num=1(第七个元素):
current_max = max(1,5+1)=6;max_sum = max(5,6)=6。
⑧ 遍历num=-5(第八个元素):
current_max = max(-5,6+(-5))=1;max_sum = max(6,1)=6。
⑨ 遍历num=4(第九个元素):
current_max = max(4, 1+4)=5;max_sum = max(6,5)=6。
最终current_max循环更新结束,max_sum始终为6,所以输出6,和动态规划、暴力法结果一致。
6. 复杂度总结
时间复杂度:O(n)(仅一层for循环,遍历数组一次,n是数组长度),是所有解法中最快的;
空间复杂度:O(1)(只用到current_max和max_sum两个变量,不占用任何额外内存),是最优的空间效率。
补充:Kadane算法是面试中最常考的解法,必须掌握,代码简洁、效率高,记住核心的状态转移逻辑即可。
解法4:分治法(进阶拓展,理解递归思想,可逐步吃透)
1. 核心思路(大白话,结合前置递归知识)
分治法的核心是"分而治之"------把一个大问题拆成两个小问题,分别解决小问题,再把小问题的结果合并,得到大问题的答案。对应这道题,具体思路如下:
① 拆分:把整个数组分成"左半部分"和"右半部分"(找数组的中间位置mid,左半是[left, mid],右半是[mid+1, right])。
② 求解子问题:递归求解"左半部分的最大子数组和"(left_max)和"右半部分的最大子数组和"(right_max)。
③ 合并:最大子数组有3种可能性,取这3种情况的最大值,就是整个数组的最大子数组和:
-
最大子数组完全在左半部分(和为left_max);
-
最大子数组完全在右半部分(和为right_max);
-
最大子数组跨越中间元素mid(和为cross_max,即左半部分"以mid结尾的最大和" + 右半部分"以mid+1开头的最大和")。
④ 终止条件:当left == right时(数组只有一个元素),最大子数组和就是这个元素本身(因为子数组最少包含1个元素)。
补充:分治法虽然不是最优解(时间复杂度比Kadane算法高),但能帮我们理解递归思想,适合进阶学习,面试中偶尔会问到。
2. C++代码(逐句注释,每句讲含义,能看懂)
cpp
#include <vector>
#include <climits> // 用到INT_MIN(最小整数),处理全负数情况
#include <algorithm> // 用到max()函数,比较多个数的大小
using namespace std;
class Solution {
private:
// 辅助函数1:计算"跨越中间元素mid"的最大子数组和(核心辅助函数)
// 参数:nums(原数组)、left(左半部分起始索引)、mid(中间索引)、right(右半部分结束索引)
int crossSum(vector<int>& nums, int left, int mid, int right) {
// 第一步:计算左半部分"以mid结尾"的最大和(从mid往左遍历,累加找最大)
int left_sum = INT_MIN; // 左半部分最大和,初始化为最小整数
int current = 0; // 临时累加和,初始化为0
// 从mid往左遍历(i从mid到left,每次减1),因为要找"以mid结尾"的最大和
for (int i = mid; i >= left; i--) {
current += nums[i]; // 累加当前元素
left_sum = max(left_sum, current); // 更新左半部分最大和
}
// 第二步:计算右半部分"以mid+1开头"的最大和(从mid+1往右遍历,累加找最大)
int right_sum = INT_MIN; // 右半部分最大和,初始化为最小整数
current = 0; // 临时累加和重置为0
// 从mid+1往右遍历(i从mid+1到right),因为要找"以mid+1开头"的最大和
for (int i = mid+1; i <= right; i++) {
current += nums[i]; // 累加当前元素
right_sum = max(right_sum, current); // 更新右半部分最大和
}
// 跨越中间的最大和 = 左半结尾最大和 + 右半开头最大和
return left_sum + right_sum;
}
// 辅助函数2:递归函数,求解区间[left, right]内的最大子数组和
int helper(vector<int>& nums, int left, int right) {
// 终止条件:当区间只有一个元素(left == right),最大和就是这个元素本身
if (left == right) return nums[left];
// 1. 找中间索引mid,拆分左右区间(整数除法,避免溢出)
int mid = (left + right) / 2;
// 2. 递归求解左半区间[left, mid]的最大和
int left_max = helper(nums, left, mid);
// 3. 递归求解右半区间[mid+1, right]的最大和
int right_max = helper(nums, mid+1, right);
// 4. 计算跨越中间元素的最大和
int cross_max = crossSum(nums, left, mid, right);
// 5. 返回左、右、跨越中间三者中的最大值,就是当前区间的最大和
return max(max(left_max, right_max), cross_max);
}
public:
// 主解题函数:调用递归辅助函数,初始区间是[0, nums.size()-1](整个数组)
int maxSubArray(vector<int>& nums) {
// 调用helper函数,传入整个数组的区间(从0到最后一个元素)
return helper(nums, 0, nums.size()-1);
}
};
// 主函数(测试函数,和前面解法一致,测试示例+自定义输入)
int main() {
Solution sol;
// 测试示例1:预期6
vector<int> nums1 = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
cout << "示例1输出:" << sol.maxSubArray(nums1) << endl;
// 测试示例2:预期1
vector<int> nums2 = {1};
cout << "示例2输出:" << sol.maxSubArray(nums2) << endl;
// 测试示例3:预期23
vector<int> nums3 = {5, 4, -1, 7, 8};
cout << "示例3输出:" << sol.maxSubArray(nums3) << endl;
// 自定义测试1:全负数,预期-2
vector<int> nums4 = {-5, -3, -2, -7};
cout << "自定义测试1输出:" << sol.maxSubArray(nums4) << endl;
// 自定义测试2:全正数,预期10
vector<int> nums5 = {1, 2, 3, 4};
cout << "自定义测试2输出:" << sol.maxSubArray(nums5) << endl;
return 0;
}
3. Python代码(逐句注释,每句讲含义,能看懂)
php
from typing import List # 引入List类型,指定参数类型
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
# 辅助函数1:计算跨越中间元素mid的最大子数组和
def cross_sum(left, mid, right):
# 计算左半部分"以mid结尾"的最大和(从mid往左遍历)
left_sum = float('-inf') # 左半最大和,初始化为负无穷(处理全负数)
current = 0 # 临时累加和,初始化为0
# 从mid往左遍历,range(mid, left-1, -1)表示从mid到left,步长为-1(往左走)
for i in range(mid, left-1, -1):
current += nums[i] # 累加当前元素
left_sum = max(left_sum, current) # 更新左半最大和
# 计算右半部分"以mid+1开头"的最大和(从mid+1往右遍历)
right_sum = float('-inf') # 右半最大和,初始化为负无穷
current = 0 # 临时累加和重置为0
# 从mid+1往右遍历,到right结束
for i in range(mid+1, right+1):
current += nums[i] # 累加当前元素
right_sum = max(right_sum, current) # 更新右半最大和
# 跨越中间的最大和 = 左半结尾最大和 + 右半开头最大和
return left_sum + right_sum
# 辅助函数2:递归函数,求解区间[left, right]的最大子数组和
def helper(left, right):
# 终止条件:区间只有一个元素,最大和就是这个元素
if left == right:
return nums[left]
# 1. 找中间索引mid,拆分左右区间(整数除法)
mid = (left + right) // 2
# 2. 递归求解左半区间[left, mid]的最大和
left_max = helper(left, mid)
# 3. 递归求解右半区间[mid+1, right]的最大和
right_max = helper(mid+1, right)
# 4. 计算跨越中间元素的最大和
cross_max = cross_sum(left, mid, right)
# 5. 返回三者中的最大值,作为当前区间的最大和
return max(max(left_max, right_max), cross_max)
# 主调用:求解整个数组的最大和,初始区间是[0, len(nums)-1]
return helper(0, len(nums)-1)
# 主函数(测试函数,和前面一致)
if __name__ == "__main__":
sol = Solution()
# 测试示例1:预期6
nums1 = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
print("示例1输出:", sol.maxSubArray(nums1))
# 测试示例2:预期1
nums2 = [1]
print("示例2输出:", sol.maxSubArray(nums2))
# 测试示例3:预期23
nums3 = [5, 4, -1, 7, 8]
print("示例3输出:", sol.maxSubArray(nums3))
# 自定义测试1:全负数,预期-2
nums4 = [-5, -3, -2, -7]
print("自定义测试1输出:", sol.maxSubArray(nums4))
# 自定义测试2:全正数,预期10
nums5 = [1, 2, 3, 4]
print("自定义测试2输出:", sol.maxSubArray(nums5))
4. 调用流程(能看懂的递归执行步骤)
以C++为例(Python递归流程完全一致,只是语法不同):
① 主函数调用sol.maxSubArray(nums),进入主解题函数,主函数调用helper(nums, 0, n-1)(n是数组长度),开始递归。
② helper函数中,先判断left是否等于right:如果是,返回当前元素;如果不是,找mid,拆分左右区间。
③ 递归调用helper(nums, left, mid),求解左半区间的最大和(left_max);再递归调用helper(nums, mid+1, right),求解右半区间的最大和(right_max)。
④ 调用crossSum函数,计算跨越mid的最大和(cross_max)。
⑤ 比较left_max、right_max、cross_max,返回三者中的最大值,作为当前区间的最大和,回溯到上一层递归。
⑥ 所有递归结束后,最终返回的就是整个数组的最大子数组和,主函数接收结果并打印。
补充:递归的核心是"先拆后合",把大数组拆成一个个小数组,直到只剩一个元素,再逐步合并结果,可以想象成"拆分积木,再拼起来"。
5. 运行过程(结合示例1,一步步拆解递归,跟着走)
示例1输入:nums = [-2,1,-3,4,-1,2,1,-5,4],n=9,初始调用helper(0,8)(left=0,right=8)。
第一步:拆分整个数组(left=0,right=8)
mid = (0+8)/2 = 4,拆分左半区间[0,4]、右半区间[5,8]。
第二步:递归求解左半区间[0,4](nums[0-4] = [-2,1,-3,4,-1])
mid = (0+4)/2 = 2,拆分左半[0,2]、右半[3,4]。
- 递归求解[0,2](nums[0-2] = [-2,1,-3]):
mid = (0+2)/2 = 1,拆分左半[0,1]、右半[2,2]。
a. 求解[2,2]:left==right,返回nums[2] = -3(left_max1 = -3);
b. 求解[0,1](nums[0-1] = [-2,1]):
mid = 0,拆分左半[0,0]、右半[1,1];
-
求解[0,0]:返回-2(left_max2 = -2);
-
求解[1,1]:返回1(right_max2 = 1);
-
计算cross_sum(0,0,1):左半结尾最大和=-2,右半开头最大和=1,和为-1;
-
0,1\]的最大和 = max(-2,1,-1) = 1(left_max3 = 1);
d. [0,2]的最大和 = max(1, -3, -2) = 1(left_max4 = 1);
- 递归求解[3,4](nums[3-4] = [4,-1]):
mid = 3,拆分左半[3,3]、右半[4,4];
a. 求解[3,3]:返回4(left_max5 = 4);
b. 求解[4,4]:返回-1(right_max5 = -1);
c. 计算cross_sum(3,3,4):左半结尾最大和=4,右半开头最大和=-1,和为3;
d. [3,4]的最大和 = max(4, -1, 3) = 4(right_max4 = 4);
-
计算cross_sum(0,2,4):左半结尾最大和([0-2]结尾最大和1)=1,右半开头最大和([3-4]开头最大和4 + (-1)=3)=3,和为4;
-
0,4\]的最大和 = max(1,4,4) = 4(left_max = 4);
mid = (5+8)/2 = 6,拆分左半[5,6]、右半[7,8]。
- 递归求解[5,6](nums[5-6] = [2,1]):
mid = 5,拆分左半[5,5]、右半[6,6];
a. 求解[5,5]:返回2(left_max6 = 2);
b. 求解[6,6]:返回1(right_max6 = 1);
c. 计算cross_sum(5,5,6):左半结尾最大和=2,右半开头最大和=1,和为3;
d. [5,6]的最大和 = max(2,1,3) = 3(left_max7 = 3);
- 递归求解[7,8](nums[7-8] = [-5,4]):
mid = 7,拆分左半[7,7]、右半[8,8];
a. 求解[7,7]:返回-5(left_max8 = -5);
b. 求解[8,8]:返回4(right_max8 = 4);
c. 计算cross_sum(7,7,8):左半结尾最大和=-5,右半开头最大和=4,和为-1;
d. [7,8]的最大和 = max(-5,4,-1) = 4(right_max7 = 4);
-
计算cross_sum(5,6,8):左半结尾最大和([5-6]结尾最大和3)=3,右半开头最大和([7-8]开头最大和-5+4=-1)=-1,和为2;
-
5,8\]的最大和 = max(3,4,2) = 4(right_max = 4);
cross_sum(0,4,8):
① 左半[0,4]结尾最大和:从mid=4往左遍历,累加结果为-1(4-1)、3(4-1+4)、0(4-1+4-3)、1(4-1+4-3+1)、-1(4-1+4-3+1-2),最大和为3;
② 右半[5,8]开头最大和:从mid+1=5往右遍历,累加结果为2、3、-2、2,最大和为3;
③ cross_max = 3 + 3 = 6;
第五步:合并结果
整个数组的最大和 = max(left_max=4, right_max=4, cross_max=6) = 6,输出6。
补充:递归过程看似复杂,但核心是"拆分到最小,再逐步合并",不用死记每一步,重点理解"左、右、跨越中间"三个情况的合并逻辑即可。
6. 复杂度总结
时间复杂度:O(nlogn)(每次拆分数组为2份,共拆分logn层,每层遍历数组一次,总操作次数为n×logn);
空间复杂度:O(logn)(递归时用到"递归栈",栈的深度就是拆分的层数,即logn)。
补充:分治法的时间复杂度比Kadane算法高,空间复杂度比Kadane算法高,所以实际面试中,优先用Kadane算法,分治法主要用来理解递归思想。
四、整体总结(必看,梳理重点,快速掌握)
这道题(最大子数组和)是算法入门的经典题,四种解法从易到难,覆盖了暴力枚举、动态规划、空间优化、递归分治,可以按以下顺序学习,逐步提升:
- 入门首选:暴力枚举法
-
优点:思路最简单、最直观,能快速理解"连续子数组"和"最大和"的核心要求;
-
缺点:时间复杂度O(n²),速度慢,不适合大数据量;
-
用途:帮建立对题目的基本认知,打好基础。
- 过渡学习:动态规划法
-
优点:时间复杂度优化到O(n),避免了暴力法的重复计算;
-
缺点:空间复杂度O(n),需要额外的dp数组;
-
用途:理解"动态规划"的核心思想(拆分子问题、状态转移),为学习Kadane算法铺垫。
- 面试必备:Kadane算法(最优解)
-
优点:时间复杂度O(n)、空间复杂度O(1),效率最高,代码简洁;
-
核心:用一个变量代替dp数组,本质是空间优化后的动态规划;
-
要求:必须熟练掌握,面试中遇到这道题,直接写Kadane算法即可。
- 进阶拓展:分治法
-
优点:能锻炼递归思维,适合理解"分而治之"的算法思想;
-
缺点:效率不如Kadane算法,实际应用较少;
-
用途:进阶学习,拓宽算法思路,应对面试中对递归的考察。