题目
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
如图:

我们在刷题的时候经常会听到这么几个词:
子数组,子序列,连续子序列。他们的区别在哪里呢?
子数组:在原数组中连续的一段
子序列:不一定连续
连续子序列:就是子数组
这几个概念搞清楚,我们一起来看看这道题:
咱们以示例1为例:
示例1中的最大子数组就是[4,-1,2,1]这一段,他的元素和是6,是所有子数组中元素和最大的,所以6就是这个问题的答案。
这个答案是怎样求得的呢,接下来我介绍三种算法来解决这个问题。
方法一:暴力算法
核心思路:就是枚举所有可能的情况,因为子数组是连续的,我们可以固定起点,遍历所有的终点,计算每一段的和,再更新记录最大值。
比如:数组长度为 n ,我们用 i 表示子数组起点,从0到 n-1 依次遍历,每固定一个 i 就让 j 从 i 遍历到 n-1 ,同时累加从 i 到 j 的和,每算一次就和当前最大值比较并更新最大值,直到 i 遍历完所有位置,这样所有连续子数组都被我们检查了一遍,最后得到的最大值就是我们要求的答案了。
代码也非常的简单:
cpp
int maxSubArray(vector<int>& nums) {
int n =nums.size();
int max = nums[0];
for(int i=0;i<n;++i)
{
int add = 0;
for(int j=i;j<n;++j)
{
add+=nums[j];
if(add>max)
{
max=add;
}
}
}
return max;
}
暴力算法的优点就是逻辑简单易于理解,缺点就是时间复杂度太大(O(n²))。
当数组长度n很大时,作为题目提交的时候可能会超时,在面试中只写这个算法也是不够的,这时我们就要引入第二种算法了。
方法二:分治算法
核心思想:把大问题拆成小问题,然后小问题解决后再合并,最终得到大问题的答案。
对于这个题目,我们可以把数组从中间分成左右两个部分,那么最大子数组只会出现在三个地方:完全在左半部分、完全在右半部分、跨越中间位置(左半部分有一点右半部分有一点,拼接起来)。所以我们只需要分别求出这三个位置的最大子数组和,然后取其中的最大值,就是整个数组的最大子数组和。

对于左半部分,是一个大问题下的子问题,可以通过递归去求解;同样右半部分也是一样,所以两边都可以递归;重点是怎么求跨越中间的最大子数组和,
这个步骤需要单独处理:我们从中间位置向左遍历并累加元素,记录过程中的最大和;再从中间位置向右遍历并累加元素,记录过程中的最大和,然后把左最大和与右最大和相加就是跨越中间的最大和;最后在把这三个部分取个最大值就是这个问题的答案了。
核心是分治法+递归的调用:
cpp
class Solution {
public:
int maxSubArray(vector<int>& nums) {
return getmaxSubArray(nums,0,nums.size()-1);
}
private:
int getmaxSubArray(vector<int>& nums,int left,int right)
{
//递归终止条件:子数组只有一个元素
if(left == right)
{
return nums[left];
}
int mid = left +(right - left)/2;
//递归计算左右子数组的最大子数组和
int left_max = getmaxSubArray(nums,left,mid);
int right_max = getmaxSubArray(nums,mid+1,right);
//计算跨中点的最大子数组和
int cross_left = INT_MIN;//INT_MIN是C++ 标准库<climits>中定义的常量,表示当前系统中int类型的最小取值
int sum=0;
for(int i=mid;i>=left;--i)
{
sum += nums[i];
if(sum > cross_left)
{
cross_left = sum;
}
}
int cross_right = INT_MIN;
sum = 0;
for(int i = mid+1;i<=right;++i)
{
sum+=nums[i];
if(sum>cross_right)
{
cross_right = sum;
}
}
int cross_max = cross_left + cross_right;
//返回三者中的最大值
return max(max(left_max,right_max),cross_max);
}
};
这个算法的优点是时间复杂度是O(nlogn)的。相比暴力算法优化了很多;
缺点就是递归的逻辑较复杂,而且递归会有额外的栈开销。
方法三:Kadane算法(重点)
这是一个动态规划的解法,也是面试过程中最常考的最优解法。
思路:我们定义 dp[ i ] 为以第 i 个元素结尾的最大子数组和,那我们要求的答案就是 dp 数组中的元素最大值,对于 dp[ i ] 来说有两种选择:
第一种:把第 i 个元素加入到前面的子数组,也就是 dp[ i -1 ] + nums[ i ];
第二种:以第 i 个元素单独作为子数组,也就是 nums[ i ]。
所以dp[ i ]其实就是dp[ i -1 ] + nums[ i ] 和 nums[ i ]之间的一个最大值,也就是max(dp[ i -1 ] + nums[ i ] ,nums[ i ])。
那为什么是这两种选择?
因为子数组它一定是连续的,既然是连续的,以 i 结尾的子数组要么包含 i-1 结尾的子数组,要么自己单独成立一个。这个算法的精妙之处就在于只需要遍历数组一次就能算出所有的dp值,时间复杂度是O(n)的。
同样是考虑了所有情况,但是通过一种递推的思维把重复的计算都省略掉了。
代码:
cpp
int maxSubArray(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n);
dp[0] = nums[0];
int maxnum = dp[0];
for(int i=1;i<n;++i)
{
//状态转移:要么延续前面的子数组,要么从当前元素重新开始
dp[i] = max(dp[i-1]+nums[i],nums[i]);
//更新全局最大和
maxnum = max(maxnum,dp[i]);
}
return maxnum
}
优化
因为要存dp数组,空间复杂度是O(n)的,但我们可以优化空间,因为计算dp[ i ]的时候,只需要用到dp[ i-1 ],所以不用保存整个数组,只需要用cur_max变量来记录 dp[ i-1 ],再用另外一个变量all_max来记录全局最大值,这样以来空间复杂度就降到了O(1)了。
代码:
cpp
int maxSubArray(vector<int>& nums) {
int n = nums.size();
//初始化全局最大值为最小整数
int all_max = INT_MIN;
//初始化当前子数组和
int cur_max = 0;
for(int i = 1;i<=n;++i)
{
cur_max = max(cur_max + nums[i-1],nums[i-1]);
all_max = max(all_max,cur_max);
}
return all_max;
}
这些就是本篇文章的内容了,希望对大家的学习有帮助~~