给你一个整数数组 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 <= 105
-104 <= nums[i] <= 104进阶:如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的 分治法 求解。
需要遍历整个数组1次,时间复杂度O(n) 。使用了两个变量保存当前和与最大和,只使用了常数空间,空间复杂度O(1)。
附上当时写题的乱七八糟的思路:
核心思想:若当前遍历到的元素之前的和小于0,则直接舍弃掉前面的数列。
class Solution {
public:
int maxSubArray(vector& nums) {
int max = nums[0];
int curSum = 0;
for(int i = 0; i < nums.size(); i++)
{
if(curSum >= 0)
{
curSum += nums[i];
}else{
curSum = nums[i];
}
if(curSum > max)
{
max = curSum;
}
}
return max;
}
};
注意:
1.max必须取nums[0],并不能取0什么的,因为nums里最大的数可能就是负数。
2.curSum>max的判定语句要放最后,因为nums[0]<0时,会直接将max更新成0了,违反了第一条
分治策略就是将问题分解为多个子问题,将子问题再分解出子问题……且这些子问题相互独立并与原问题形式相同或相似,递归地解决这些子问题后,将各个子问题的解合并得到原问题的解。
像排序算法中涉及到的分治策略有:二分法、快排和希尔排序等。
分治策略的步骤通常为以下三步:
分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;
解决:若子问题规模较小而容易被解决则直接解,否则【递归地】解各个子问题;
合并:将各个子问题的解合并为原问题的解。
而回到原题,对于区间(l,r),可以分解为左(l,m)和右(m+1,r)两个区间,再对这两个区间分别再不断拆分至规模足够小的子问题得到直接解……因此可以找到递归终止,也就是不可再拆分的条件为:当区间长度为1,left==right。同时,对于(l,r),最大子数组之和可能恰巧在左区间内,也可能在右区间内,甚至可以出现在左区间的后半段+右区间的前半段。因此,其出现情况又大致可以分成两种:
1.跨越了中点m。→③以中点为起始点,向前后左右区间两部分分别往下延伸找
2.不跨越m。 →找①左/②右区间的最大子数组和
最后,再比较这三种结果,取最大者。
class Solution {
public:
int findSpan(vector& nums, int left, int right, int mid){
int curSum = nums[mid];
int leftSum = nums[mid], rightSum = nums[mid];
for(int i = mid - 1; i >= left; i--)
{
curSum += nums[i];
if(curSum > leftSum)
leftSum = curSum;
}
curSum = nums[mid];
for(int i = mid + 1; i <= right; i++)
{
curSum += nums[i];
if(curSum > rightSum)
rightSum = curSum;
}
return (leftSum + rightSum - nums[mid]);
}
int maxSum(vector& nums, int left, int right){
if(left == right)
{
return nums[left];
}else{
int mid = (left + right) / 2;
int left_max,right_max,mid_max;
left_max = maxSum(nums,left,mid);
right_max = maxSum(nums,mid+1,right);
mid_max = findSpan(nums,left,right,mid);
return max(mid_max,max(left_max,right_max));
}
}
int maxSubArray(vector& nums) {
return maxSum(nums,0,nums.size()-1);
}
};
时间复杂度O(n^2)。就是超时了:D
class Solution {
public:
int maxSubArray(vector& nums) {
int max = nums[0];
int sum;
for(int i = 0; i < nums.size(); i++)
{
for(int j = i; j < nums.size(); j++)
{
sum += nums[j];
max = sum > max ? sum : max;
}
sum = 0;
}
return max;
}
};
先来了解一下动态规划的概念,以下一段来自百科:
基本思想:动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。
通俗的说就是,你要做出一个宏大的决策,那么可以先试图将该决策拆分成多个阶段的决策问题,且每个阶段都会或多或少的影响下一个阶段的决策,因此可以保留前一个阶段的决策的答案,避免重复的计算。
最大子序和这道题就很适合举例,接触下来我觉得动态规划和贪心策略很相像。动态规划有两种特征:1)重复子问题2)最优子结构。而贪心策略是对于问题的求解,总是做出当前的最优解,而不从整体考虑,其特征也有两个:1)贪心选择策略2)最优子结构。
但要注意的是,贪心策略并不总能得到问题的整体最优解,因此对于一个问题能否用贪心算法,必须证明每一步所作的贪心选择会最终导致问题的整体最优解。
思路:
dp[i]是指以nums[i]结尾的最大子序和。据说找最大最小值时,要将初始值定义为INT_MAX 或者INT_MIN。
class Solution {
public:
int maxSubArray(vector& nums) {
int maxSum = INT_MIN;
vectordp(nums.size());
dp[0] = nums[0];
maxSum = nums[0];
for(int i = 1; i < nums.size(); i++){
dp[i] = max(dp[i-1]+nums[i],nums[i]);
maxSum = max(dp[i],maxSum);
}
return maxSum;
}
};
时间复杂度O(n) 空间复杂度O(n)。但是空间复杂度可以优化为O(1),因为只需要知道前一项dp[i-1]的大小,所以可以用整型数据代替数组。