题目地址:力扣
暴力求解的方法很容易想到,就是用滑动窗口的方法,但是时间复杂度太高了,因此这里就不写了。这道题主要可以用两种方法求解,一种是贪心法,一种是动态规划。
贪心法
后向贪心
这是我自己想出来的解法,后向贪心比较啰嗦,注意的细节也比较多。
思想如下:
第一层遍历从前向后找,如果碰到的单个元素大于全局最大值,那么就将其赋值给全局最大。
如果碰到的元素是小于等于0的,就continue,因为这样的元素加入子数组中一定是没贡献的。
如果碰到的元素是大于0的,那么进入内循环,内循环有一个子数组值tmp,其初始值就是当前碰到的元素。循环条件是tmp大于0且没有循环下标没越界。循环体内就将下一个元素和tmp相加。如果tmp比全局最大值还大,就将其赋值给全局最大。直到tmp小于等于0或者循环到数组末尾,因为tmp一旦小于等于0,就说明当前数组是没贡献的。因此子数组只需要从下一个大于0的元素算起即可。这里的关键点要想明白,如果子数组一旦小于0了,那么跳出循环,中间的所有元素都直接跳过不管了,而是从下一个大于0的元素开始算。因为整个子数组小于等于0,而子数组的第一个元素大于0,因此从子数组中间任何一个位置开始往后取值都一定是小于0的。
这个方法有点绕,但是思路也是没问题的,代码如下
class Solution {
public:
int maxSubArray(vector<int>& nums) {
// 将全局最大初始化为一个最小值
int biggest = INT32_MIN;
// 第一层遍历
for (size_t i = 0; i < nums.size(); ++i)
{
// 判断当前元素与全局最大的关系
if (nums[i] > biggest)
biggest = nums[i];
// 如果当前元素小于等于0,就跳过它
if (nums[i] <= 0)
continue;
else
// 当前元素大于0
{
// 初始化子数组值为当前元素值
int tmp = nums[i];
// 子数组值大于0且未越界
while ( tmp > 0 && ++i < nums.size())
{
tmp += nums[i];
// 如果子数组值比全局最大值大,则赋值
if (tmp > biggest)
{
biggest = tmp;
}
}
}
}
return biggest;
}
};
前向贪心
这是看了力扣上别人的解法,前向贪心和后向贪心最大的区别就在于。后向贪心是遍历到当前元素之后往后找。而前向贪心是遍历到当前元素之后往前看。
思想如下:
首先将数组的第一个元素赋给全局最大值和子数组。
从第二个元素开始遍历,循环条件是子数组值加上当前元素值比当前元素值要大。这就保证了无论当前元素是正还是负,子数组值一定是正的。因此可以将子数组向后扩展。如果条件不满足,则说明子数组值是负的,那么舍弃之前的子数组,从当前元素开始,重新计算子数组值。比较子数组值与全局最大以更新全局最大值。
代码如下:
class Solution {
public:
int maxSubArray(vector<int>& nums) {
// 全局最大值和之前的子数组值
int biggest = nums[0];
int tmp = nums[0];
// 遍历所有元素
for (size_t i = 1; i < nums.size(); ++i )
{
// 如果之前的子数组值加上当前元素值比当前元素值大,那么子数组继续扩展
if (tmp + nums[i] > nums[i])
tmp += nums[i];
// 否则子数组从当前元素重新开始算
else
tmp = nums[i];
// 如果子数组值比全局最大值大,则更新全局最大值
if (tmp > biggest)
biggest = tmp;
}
return biggest;
}
};
前向贪心还有另一种思路,即考虑子数组值来进行判断,而非考虑当前元素值进行判断的方法,其代码如下
class Solution {
public:
int maxSubArray(vector<int>& nums) {
// 全局最大值和之前的子数组值
int biggest = nums[0];
int tmp = nums[0];
// 遍历所有元素
for (size_t i = 1; i < nums.size(); ++i )
{
// 子数组扩充的情况
if (tmp >= 0 && tmp + nums[i] > 0)
{
tmp += nums[i];
// 判断子数组值和全局最大值的大小
if (tmp > biggest)
biggest = tmp;
}
// 子数组重置的情况
else
if (nums[i] > 0)
tmp = nums[i];
else
tmp = 0;
// 判断当前元素和全局最大值的大小
if (nums[i] > biggest)
biggest = nums[i];
}
return biggest;
}
};
动态规划
DP的题目写的比较少,看了力扣上一位大神的讲解感觉豁然开朗,DP讲解:力扣
其核心思想就是:把原问题拆解为很多子问题,原问题的解可以由一个个的子问题求得,这里原问题是:最大的连续子数组的值是多少;子问题是:以某个数结尾的连续子数组的最大和是多少?
这样拆解问题之后,我们只需要遍历整个数组,就可以求得子问题的解。原问题的解就是所有子问题解的最大值。可以说DP的方法非常优雅了。其实这种动态规划的方法代码写出来和前向贪心的的代码几乎一模一样。
其代码如下:
class Solution {
public:
int maxSubArray(vector<int>& nums) {
// 全局最大值和上一个子问题解的值
int biggest = nums[0];
int tmp = nums[0];
// 遍历整个数组
for (size_t i = 1; i < nums.size(); ++i)
{
// 若上一个子问题解大于0,那么就把它和当前元素值加起来得到当前子问题的解
if (tmp > 0)
tmp = tmp + nums[i];
// 若上一个子问题解不大于0,则当前子问题解就等于当前元素值
else
tmp = nums[i];
// 通过不断更新全局最大值,以得到原问题的解
if (tmp > biggest)
biggest = tmp;
}
return biggest;
}
};
分治法
这种方法写起来比较麻烦,而且复杂度也达到了O(nlogn),但是思想是线段树的思想,还是可以看看的。
思想如下:
将nums数组分治地处理,即从中间一刀切开为二,直至需要处理的子数组只有一个元素。
这种方法实际处理起来是这样考虑的,最大子数组要不在切开的左边,要不在切开的右边,要不就覆盖了切开的地方并向从切开点向左右延申。将一个数组上下标传入函数,那么切开位置就是mid,通常mid = (left + right) / 2,那么如果是nums的个数为奇,则mid恰好是中间元素。如果是偶数,那么mid就是中间偏左的第一个元素(因为向下取整)。但是奇偶都不影响操作。
只需要找到切开点左边最大子数组值,再找到切开点右边最大子数组值。(左右两边只需要有一边包括切开点就行)。然后剩下的关键一步就是以切开点为中心,向左右两边蔓延,寻找向左蔓延的最大子数组值和向右蔓延的最大子数组值,将两者相加就是覆盖了切开点的最大子数组值。剩下的操作就是比较这三者,选出最大的作为最终的答案返回。
class Solution {
public:
int maxSubArray(vector<int>& nums) {
// 返回最大值
int biggest = computeLR_Max(nums, 0, nums.size()-1);
return biggest;
}
// 这个函数用于找左边子数组和右边子数组的最大值
int computeLR_Max(vector<int> &nums, int left, int right)
{
// 只剩一个元素的时候,就直接返回这个元素
if (right - left == 0)
return nums[left];
int mid = (left + right) / 2;
// 找左边子数组最大值
int lmax = computeLR_Max(nums, left, mid);
// 找右边子数组最大值
int rmax = computeLR_Max(nums, mid + 1, right);
// 找覆盖了切开点的子数组最大值
int mmax = computeM_Max(nums, left, mid, right);
// 返回的是三者中的最大值
return lmax > rmax ? (lmax > mmax ? lmax : mmax) : (rmax > mmax ? rmax : mmax);
}
/* 这个函数用于找覆盖了切开点的子数组最大值,注意这里的left, mid, right下标
不能再使用size_t了,因为size_t是无符号的,函数体扩展的时候会导致变量越过0
变成最大值,函数无限循环*/
int computeM_Max(vector<int>& nums, int left, int mid, int right)
{
int left_max = INT_MIN;
int sum = 0;
// 向左边一直扩展直到尽头,得到left_max
for (int i = mid; i >= left; --i)
{
sum += nums[i];
if (sum > left_max)
left_max = sum;
}
int right_max = INT_MIN;
sum = 0;
// 向右边一直扩展直到尽头,得到最大值right_max
for (int i = mid + 1; i <= right; ++i)
{
sum += nums[i];
if (sum > right_max)
right_max = sum;
}
// 左右加起来,得到包含切开点的子数组最大值
return left_max + right_max;
}
};
Accepted
- 209/209 cases passed (92 ms)
- Your runtime beats 54.91 % of cpp submissions
- Your memory usage beats 89.33 % of cpp submissions (66 MB)