求给定数组的最大子序和
题目描述:
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
进阶:
如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/maximum-subarray
暴力实现法:
分析:要找出数组中的最大自序和,最直接的就是将每个元素向后求长度为1,为2,……等的求和,取最大序和,枚举出每段的累加和,这样每个元素都对应一个向后遍历的最大自序和,一共有numsSize!个值,然后在所有元素对应的最大自序和中取最大的作为该数组的最大自序和,这样简单暴力的思想也很容易想到。
代码实现:
int add_arr_i_j(int *arr, int start, int end)//计算并返回start到end下标的累加和
{
int num = 0;
int i = 0;
for (i = start; i <= end; i++)
{
num += arr[i];
}
return num;
}
int maxSubArray(int* nums, int numsSize)
{
int i = 0;
int num = 0;//num是从i-j的子序列和
int num_max = nums[0];//记录每个i值的最大序列和
int arr_max = nums[0];//整个数组中的子序列最大和
for (; i < numsSize; i++)
{
int j = 0;
//num_max = nums[0];
//计算从i到n中的最大子序列,并将下标pos入vector
for (j = i; j < numsSize; j++)
{
//计算i-j的和
num = add_arr_i_j(nums, i, j);//num为求出的和
if (num_max < num)//在一行队列中挑选最大的
{
num_max = num;
}
}
if (arr_max < num_max)//取每个i个最大子序列,在每行队列中最大的数组成的数组中选出最大的
{
arr_max = num_max;
}
}
return arr_max;
}
通过从前向后遍历每个元素,选取该元素开始向后的最大自序和,然后再这些元素的最大自序和中选取最大的作为该数组的最大子序和。算法简单暴力,只是在LeetCode上提交的时候不能通过,原因是时间复杂度太高为O(n^3),还是时间复杂度的问题,那么如何提高时间复杂度呢?可以用新的数组空间保存从数组首地址开始的累加和,每一段的累加和就为array[end]-arrayp[start],可使时间复杂度降为O(n^2)。那么是否可以在不增加空间复杂度的情况下降低时间复杂度呢?
动态规划方法:
从前向后遍历,用tmp_sum变量记录向后遍历的累加和(就是每段开始到该下标的累加和),若在this_sum的累加过程中遇到前一个this_sum为负数时,该this_sum就不能给后续的累加带来增益,因此放弃前面的this_sum(可置0或赋下一个数组值),有一种去其糟粕的意思 ! 同时也标示着this_sum开始记录一个新段的累计和。
用max_sum记录第一段的最大自序和,在某段this_sum的累加过程中当this_sum大于max_sum时,更新max_sum的值 ;最后得到的max_sum值为数组中最大自序和的值。
整体思想就是tmp_sum不断更新记录每一个段的自序和,并不断更新max_sum的值,使max_sum取这些自序和中最大的。最后得到最大自序和!
LeetCode题解中的一个帮助理解的例子:
状态转移方程为: tmp_sum = max(tmp_sum+ nums[i], nums[i])
代码实现:
int FindGreatestSumOfSubArray(vector<int> array)
{
int i = 0;
int max_sum = array[0];
int this_sum = array[0];
for (i = 1; i < array.size(); i++)//循环遍历数组,
{
if (this_sum + array[i]>array[i])//如果当前叠加的值比上一个叠加的值小,说明this_sum为负值,则重新开始
{
this_sum += array[i];//此时将最大的值赋给max_num
}
else
{
this_sum = array[i];
}
//this_sum每次记录当前叠加之后的值和array[i]之间的较大值,当前面的记录的值为负值时,后面重新开始this_sum=array[i];(既往不咎,因为前面的为负,计算上前面的值之后只会越来越小)
if (max_sum < this_sum)//每累加一次,对max_sum进行一次判断更新
{
max_sum = this_sum;
}
}
return max_sum;//返回最大的叠加值
}
以示例做例子图解:
从图中可以看出示例中max_sum取自第三段的最大自序和。
分析上面的算法时间复杂度是O(n),已经可以通过LeetCode的提交了,那么是否还有更优越的算法?从题中我们无从得知这个最大子序列是从哪个下标开始,哪个下标结束,而且上面的动归算法已经提示我们最大自序和来自数组中的某一段,那我们可以先将该数组分成许多小段,然后分别计算这些分段的最大自序和,再计算合段后的最大自序和,取最大的一段作为最大子序和。这就是分治法的思想。
分治法
将数组拆分为小段,每次合段的时候比较两端的最大自序和及合段后的最大自序和,合段后的最大自序和就是从mid向两端遍历不断累加的最大值,返回三数中的最大值作为该段的最大子序和。然后不断向上层递归可得到整个数组的最大自序和。
int dealmaxSubArray(vector<int> &nums,int start,int end)
{
if (start >= end)
{
cout << start << " nums[start]:";
cout << nums[start] << endl;
return nums[start];
}
int i = 0;
int mid = start + (end - start) / 2;
int le_val=dealmaxSubArray(nums,start,mid);//返回左段最大子序和
int rg_val=dealmaxSubArray(nums, mid+1, end);//返回右段最大自序和
int sum = 0;
int tmpmax = nums[mid];
int crossmax = 0;//记录合段后的段最大自序和
for (i = mid; i >= start; i--)
{
sum += nums[i];
if (sum > tmpmax)
{
tmpmax = sum;
}
}
crossmax = tmpmax;
sum = 0;
tmpmax = nums[mid+1];
for (i = mid + 1; i <= end; i++)
{
sum += nums[i];
if (sum > tmpmax)
{
tmpmax = sum;
}
}
crossmax += tmpmax;
return max(max(le_val, rg_val), crossmax);
}
int maxSubArray(vector<int>& nums)
{
int n = nums.size();
int val=dealmaxSubArray(nums, 0, n-1);//下标从0~n-1
return val;
}
分治法的时间复杂度为O(nlogN),相比于递归其实也差不多,不过比较不容易想到。
这里安利一个Github上的题解:https://github.com/azl397985856/leetcode/blob/master/problems/53.maximum-sum-subarray-cn.md#解法二—前缀和–暴力解里面的讲的解法三 - 优化前缀算法还是挺漂亮的。