题目
给定一个整数数组 nums
,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释:
连续子数组 [4,-1,2,1] 的和最大,为 6。
函数原型
C的函数原型:
int maxSubArray(int* nums, int numsSize){}
边界判断
int maxSubArray(int* nums, int numsSize){
if( nums == NULL || numsSize <= 0 )
return 0;
}
算法设计:枚举
思路:最大子序和,总是分为俩部分:[起始日,终止日](数组里第 n 个元素,即第 n+1 天)。
- 起始日可以是第
1
天到第numsSize
天中的任意一天; - 终止日只要不早于起始日即可;
- 如果起始日、终止日能够确定,从起始日到终止日,做一次连加,就算出某个特定的起始日到终止日之间股票的和。
#define max(x, y) ((x)>(y)?(x):(y))
int maxSubArray(int* nums, int numsSize){
if( nums == NULL || numsSize <= 0 ) // 判断边界
return 0;
int sum = 0;
int max_sum = -2147483647; // 不要问我为什么选这个数(INT_MIN),可以试试 max_sum = 0 --> 类似寻找最大最小值的题目,初始值一定要定义成理论上的最小最大值
for(int i=0; i<numsSize; i++){ // 枚举起始日
for(int j=i; j<numsSize; j++){ // 枚举终止日
sum = 0;
for(int l=i; l<=j; l++){ // 确定[起始日, 终止日]后,累加
sum += nums[l];
}
max_sum = max(sum, max_sum); // 选出所有日期里面最大的一个, 获取[起始日,终止日]只需要定义俩个变量,在这里接收[i, j]即可
}
}
return max_sum;
}
枚举的复杂度:
- 时间复杂度: Θ ( n 3 ) \Theta(n^{3}) Θ(n3)
- 空间复杂度:
Θ
(
1
)
\Theta(1)
Θ(1)
算法设计:枚举优化
我们可是在做面试题, Θ ( n 3 ) \Theta(n^{3}) Θ(n3) 的复杂度,不太可能过关。
应该再分析一下题目,看一下能不能改进算法。
nums[i..j]
的总和与前面的已计算出来的总和( nums[i...j-1]
)密切相关。
#define max(x, y) ((x)>(y)?(x):(y))
int maxSubArray(int* nums, int numsSize){
if( nums == NULL || numsSize <= 0 )
return 0;
int sum = 0;
int max_sum = INT_MIN;
for(int i=0; i<numsSize; i++){
sum = 0;
for(int j=i; j<numsSize; j++){
sum += nums[j]; // sum = nums[i...j] 的总和
max_sum = max(sum, max_sum);
}
}
return max_sum;
}
起始日大概只能枚举,但终止日的选法还是有技巧的。
假设起始日是第 4
天,那终止日如何定呢?
终止日是定为第 n
天,还是第 n+1
天呢?
主要是要看第 n+1
天的情况。
其实,每天都分三种情况:
- 第
n+1
天是正的,终止日定为第n+1
天。 - 第
n+1
天是负的,不要直接把终止日定为第n
天,而后应该往后看,直到某一天的累计超过sum
,再更新sum
。 - 第
n+1
为零,忽略。
#define max(x, y) ((x)>(y)?(x):(y))
int maxSubArray(int* nums, int numsSize){
if( nums == NULL || numsSize <= 0 )
return 0;
int sum = 0;
int max_sum = -2147483647;
for(int j=0; j<numsSize; j++){
sum = 0;
for(int i=j; i<numsSize; i++){
if( sum > 0 )
sum += nums[i];
else
sum = nums[i];
max_sum = max(sum, max_sum);
}
}
return max_sum;
}
枚举优化的复杂度:
- 时间复杂度: Θ ( n 2 ) \Theta(n^{2}) Θ(n2)
- 空间复杂度:
Θ
(
1
)
\Theta(1)
Θ(1)
算法设计:动态规划
思路:在整个数组或在固定大小的滑动窗口中找到总和或最大值或最小值的问题可以通过动态规划(DP)在线性时间内解决。
采用 “动态规划” 的思想,先考虑俩个问题:
- 如何设计状态
- 状态 i 的出处
解 1: 以 nums[i]
表示 [0, i]
位的最大子序和,以 nums[i]
结尾。
解 2: 状态 i
,一般情况是从 i-1
而来即,nums[i-1]
,特殊一点是从自己开始。
- 创建一个nums数组,nums[i] 是计算数组第
i
个的连续和 - nums[i] 其实只有俩种情况,是前
i-1
个连续和 + 第i
个大,or 第i
个大 nums[i] = max(nums[i-1]+nums[i], nums[i])
#define max(x, y) ((x)>(y)?(x):(y))
int maxSubArray(int* nums, int numsSize){
if( nums == NULL || numsSize <= 0 )
return 0;
int sum = nums[0];
int max_sum = nums[0];
for(int i=1; i<numsSize; i++){
if( sum > 0) // 第 i 天是正数
sum += nums[i];
else // 如果是负数,那抛弃之前的,从当前天开始往后走
sum = nums[i];
max_sum = max(sum, max_sum);
}
return max_sum;
}
动态规划的复杂度:
- 时间复杂度: Θ ( n ) \Theta(n) Θ(n)
- 空间复杂度:
Θ
(
1
)
\Theta(1)
Θ(1)
算法设计:分治
思路:把数组元素的长度从中间一分为二,变成两个阶段,而两个小问题加起来也要比一个大问题简单些。然后再把两个小问题分成四个更小的问题,直到分不下去为止。
思考一下,存储基本点的数组最小的情况是什么?
即,结束条件:
- 没有元素,是空数组
- 只有一个元素,只需要判断和
INT_MAX
比谁大,选最大值
考虑最常见的情况,
- 数组
nums
有俩个元素及以上,显然还需要计算,所以不是最小情况,最小情况是没有元素、只有一个元素。
nums |
---|
分解后 :
a | b |
---|
解决后:
m a x a max_{a} maxa | m a x b max_{b} maxb |
---|
在 a
区间,我们找到的最大子数组和,肯定是子数组 a
中的 某个区间 [Low, Mid]
。
在 b
区间,我们找到的最大子数组和,肯定是子数组 b
中的 某个区间 [Mid+1, High]
。
还有一种因分解而来的情况没考虑,如果子数组 a
的 终止日是a
的最后一个元素,那么子数组b
的开始元素:
m a x a − m i d − b max_{a-mid-b} maxa−mid−b |
---|
是正数还可以继续衔接呀,加一个正数自然大于之前的值;相反是负数,也得继续扫描因为后面的正数可能会大于俩者之间的负数而形成新的最大子数组和, [ L o w , M i d ] ∪ [ M i d + 1 , H i g h ] [Low, Mid] \cup [Mid+1, High] [Low,Mid]∪[Mid+1,High]
合并:
上面的分解,我们算出来了 3 种情况的最大数组和,现在比较这 3 个最大数组和,答案就出来了,不是吗?
分治算法模版:
void solve(p) // p表示问题的范围、规模或别的东西。
{
if( finished /* p的规模够小 */ ){
// 用简单的办法解决
}
// 分解: 将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题
// 一般把问题分成规模大致相同的两个子问题。
for(int i=1; i<=k; i++) // 把p分解,第i个子问题为pi
// 解决: 若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
for(int i=1; i<=k; i++)
solve(pi);
// 合并: 将各个子问题的解合并为原问题的解。
......
}
实现:
#define max(x, y) ((x)>(y)?(x):(y))
#define MAX(x,y,z) (x>y?x:y)>z?(x>y?x:y):z
// MAX宏为了可读性,舍弃了安全性,没加括号
int max_array(int* array, int l, int r)
{
if(l > r) return 0;
if(l == r) return max(array[l], INT_MIN);
int lmax,sum,rmax,m;
m = (l + r) / 2;
lmax = INT_MIN;
sum = 0;
for(int i = m ; i >= l ; --i){
sum += array[i];
lmax = max(lmax, sum);
}
rmax = INT_MIN;
sum = 0;
for(int i = m + 1 ; i <= r ; ++i){
sum += array[i];
rmax = max(rmax, sum);
}
return MAX(lmax + rmax, max_array(array, l, m), max_array(array, m+1, r));
}
int maxSubArray(int* nums, int numsSize){
int max_sum = 0;
max_sum = max_array(nums, 0, numsSize-1);
return max_sum;
}
分治的复杂度:
- 时间复杂度: Θ ( N ∗ l o g N ) \Theta(N*log~N) Θ(N∗log N)
- 空间复杂度:
Θ
(
l
o
g
N
)
\Theta(log~N)
Θ(log N)
算法设计:贪心
思想:
从左往右扫描一次,遍历时并在每个步骤中更新:
- 当前元素
- 当前元素位置的最大和
- 迄今为止的最大和
可以找到所遇到的最大总和子数组。
#define max(x, y) ((x)>(y)?(x):(y))
int maxSubArray(int* nums, int numsSize)
{
int max_sum = INT_MIN, sum = 0;
for(int i = 0; i < numsSize; ++i){
sum += nums[i]; // 第一个赋值语句
max_sum = max(max_sum, sum);
if( sum < 0 ) // 如果是负数,就重新查找子序串
sum = 0;
}
return max_sum;
}
关键在于 sum
,在循环语句的第一个赋值语句之前,sum
是结束位置 i-1
的最大子序和,赋值后,sum
是结束位置 i
的最大子序和。
- 如果
sum + nums[i] > 0
,继续往后面加 - 如果
sum + nums[i] < 0
,就重新查找子序串,sum = 0
贪心的复杂度:
- 时间复杂度: Θ ( n ) \Theta(n) Θ(n)
- 空间复杂度: Θ ( 1 ) \Theta(1) Θ(1)