问题描述
给定一个整数数组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
- 输入:
约束条件
1 <= nums.length <= 10^5
-10^4 <= nums[i] <= 10^4
解决方案
方法一:动态规划
动态规划是解决最大子数组和问题的经典方法。其核心思想就是一句话:沉没成本不参与未来决策,即当前子数组的和如果小于当前元素值,则重新开始计算子数组的和。
算法逻辑
- 初始化两个变量:
pre
表示以当前元素结尾的最大子数组和,result
表示全局最大子数组和。 - 遍历数组:
- 对于每个元素
nums[i]
,更新pre
为pre + nums[i]
和nums[i]
中的较大值。 - 更新
result
为result
和pre
中的较大值。
- 对于每个元素
- 返回
result
。
代码实现
int maxSubArray(int* nums, int numsSize) {
int pre = 0, result = nums[0];
for (int i = 0; i < numsSize; i++) {
pre = (pre + nums[i]) > nums[i] ? (pre + nums[i]) : nums[i];
result = result > pre ? result : pre;
}
return result;
}
方法二:分治法
分治法通过递归将数组分成左右两部分,分别计算左右子数组的最大子数组和,再将左右子数组的最大子数组和合并。
数据结构定义
定义一个结构体Status
,用于存储子数组的四种状态:
lSum
:以左端点为起点的最大子数组和。rSum
:以右端点为终点的最大子数组和。mSum
:子数组内的最大子数组和。iSum
:子数组的总和。
合并逻辑
定义一个函数pushUp
,用于合并左右子数组的状态:
iSum
:当前子数组的总和等于左右子数组总和之和。lSum
:以左端点为起点的最大子数组和等于左子数组的lSum
和左子数组总和加上右子数组的lSum
中的较大值。rSum
:以右端点为终点的最大子数组和等于右子数组的rSum
和右子数组总和加上左子数组的rSum
中的较大值。mSum
:子数组内的最大子数组和等于左子数组的mSum
、右子数组的mSum
和左子数组的rSum
加上右子数组的lSum
中的较大值。
递归逻辑
定义一个函数get
,用于递归计算子数组的状态:
- 如果子数组只有一个元素,返回该元素作为所有状态值。
- 否则,计算中间点,递归获取左右子数组的状态,再调用
pushUp
合并左右子数组的状态。
代码实现
struct Status {
int lSum, rSum, mSum, iSum;
};
struct Status pushUp(struct Status l, struct Status r) {
int iSum = l.iSum + r.iSum; // 当前子数组的总和
int lSum = fmax(l.lSum, l.iSum + r.lSum); // 左侧最大子数组和
int rSum = fmax(r.rSum, r.iSum + l.rSum); // 右侧最大子数组和
int mSum = fmax(fmax(l.mSum, r.mSum), l.rSum + r.lSum); // 当前子数组内的最大子数组和
return (struct Status){lSum, rSum, mSum, iSum};
}
struct Status get(int* a, int l, int r) {
if (l == r) { // 如果子数组只有一个元素
return (struct Status){a[l], a[l], a[l], a[l]}; // 返回该元素作为所有状态值
}
int m = (l + r) >> 1; // 计算中间点
struct Status lSub = get(a, l, m); // 递归获取左子数组的状态
struct Status rSub = get(a, m + 1, r); // 递归获取右子数组的状态
return pushUp(lSub, rSub); // 合并左右子数组的状态
}
int maxSubArray(int* nums, int numsSize) {
return get(nums, 0, numsSize - 1).mSum; // 获取整个数组的最大子数组和
}
作者:力扣官方题解
链接:https://leetcode.cn/problems/maximum-subarray/solutions/228009/zui-da-zi-xu-he-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
方法二的优势
虽然方法二的时间复杂度与方法一相同,但由于使用了递归和结构体,运行时间略长,空间复杂度也不如方法一优秀,且难以理解。然而,方法二的优势在于它可以解决任意子区间的问题。如果将所有子区间的信息用堆式存储的方式记忆化下来,建成一棵线段树,就可以在O(logn)
的时间内求到任意区间内的答案,甚至可以修改序列中的值,做一些简单的维护,之后仍然可以在O(logn)
的时间内求到任意区间内的答案。这种方法在大规模查询的情况下具有明显的优势。
总结
最大子数组和问题可以通过动态规划和分治法解决。动态规划方法简单易懂,时间复杂度低,适合求解整个数组的最大子数组和。分治法虽然复杂,但可以解决任意子区间的问题,通过构建线段树,可以在大规模查询的情况下提高效率。