一道算法题
在CSND旗下的庞果网上,看到一道名为“求连续子数组最大和”的算法题。其问题描述如下:
一个整数数组,输出其连续子数组最大和。如2, -3, 4, 5, -4, 6,输出11.要求时间复杂度为O(N).
在解决这道题的过程中,得到关于时间复杂度O(N)的思考。下面是整个解决问题的思考过程。
起初我的思路大致是:我要只遍历一遍整数数组,可以设置两个工作指针,分别从数组首尾出发,交替地向中间移动,在此过程中记录出现的最大和,并随着移动而修改最大和,直到两个指针相遇。在陷入控制指针移动并记录合适的相关数据的比较混乱的思维泥潭后,我开始关注题目中要求的时间复杂度O(N)的意义的思考。下面是想到的:
- 先确定复杂度要求,再确定思考方向。
- 对于O(n) 复杂度,应该对 O(n)本身要有深入思考,这样会对后面思考算法有极大的指导思路。 O(n)不是说所设计的算法只对数组遍历一边,而是可以遍历多遍,但遍数值要确定,是常量。这一点虽然早就知道,但是固有思维的偏见总是一看到O(N),就默认要只遍历一遍的思考方法。专门思考将时间负责度O(N)提出来审视思考,有助于彻底改变面对O(N)要求的算法的思考方式。
- 这样便可不用将思路集中在只想该如何求得最大和。就好比从河里捕鱼,不用眼睛只寻找鱼。可以观察整个河,思考与鱼有什么关系,可不可以先干点别的为捕鱼服务。发现其实河水是不需要的,没有就最好不过了。所以我可以先挖个坑把水放了,第二步再直接捡鱼。
- 回到此问题上,思考这些整数跟最终的连续子数组最大和有什么关系,期望得到的是一个最大和,而非由哪些数组成的最大和,想到相邻的正数(包括0)完全可以相加合并成一个正数,而对于相邻的负数,如果最大和包含它们,也必是因为它们两侧都有正数,并且一定包含两侧的正数,这样的话,相邻负数也完全可以相加合并成一个负数。
- 第一遍遍历数组先把连续的同符号的数合并,用他们的和代替,对于我希望求的结果而言,他们没有必要单独存在。也许我在这步还没想到这样会为后面解题思路产生极大帮助,但至少它让数据变的简单许多。
- 合并后,数据变的正数和负数交替出现,观察发现,数组首尾如果是负数,是完全可以删掉了,不影响计算。
- 此时,数组首尾都为正数,且正数和负数交替出现,观察尝试发现一旦出现“正数+负数+正数”,我便可很容易确定其这3个数的连续最大和,同时并合并成一个正数(最大和,或者右边的正数),而且此数可以与后面两个数(一负一正)继续执行和前面一样的合并,并修改最大和,然后继续往后迭代计算。
综上,复杂度O(N)就变成了O(N+2+N),符合题意。将时间复杂度分解的过程也是将大问题分解成小问题解决,再合并的过程。
解题思路与程序设计之间的纽带
思考到这里,我本已经很满足了,觉得搞的差不多了。可当我开始实现其代码时,发现似乎还不是像上面分解的小问题这样清晰流畅。在写代码的过程中,我发现造成这个情况的原因是人脑构思出的解题思路与程序的清晰表达之间还需要纽带,我起初并没有注意它。那么这个纽带到底是什么?一个解题思路中的一个步骤其实都可以说对应一个考虑完备(应对全部可能的情景)的程序。而这样一个程序当作一个逻辑功能,用一个函数去完成其实还是比较困难的,写起来并不流畅。而如果一个函数只处理一种情景,写起来就容易对应人脑思维的步骤。而一般一个逻辑步骤对应的多种情景是由一个普通情景和一个或多个特殊情景组成。一个程序把处理情景和完成逻辑功能揉和到一块时就产生了我们经常感叹的想明白容易,写程序难。怎么自然地想到将其分离呢?还是对时间复杂度O(N)的思考。它可以分解为多个1或N的组合的本质意义是,每个分解的 1或N既可以是一个处理普通场景的步骤,也可以是处理一个特殊场景。纽带就是归纳每个分解步骤的场景。
下面将上面的解题思路结合场景归纳一次:
- 场景:全是负数,全是正数,正负数都有
- 步骤一:判断数组是否全部为负数,若是,返回最大值,即为最终要求结果。
- 场景:全是正数,正负数都有
- 步骤二:合并相邻同符号数
- 场景:只有一个正数,多个正负数交替出现
- 步骤三:如果数组首尾的数为负,则删掉。
- 场景:只有一个正数;多个正负数交替出现,且首尾都是正数。
- 步骤四:判断数组是否只有一个数,若是,返回此数,即为最终要求结果。
- 场景:多个正负数交替出现,且首尾都是正数,且个数是大于等于3。
- 步骤五:每3个数合并,再与后面两个数合并,重复迭代,知道合并结束,返回最大和。
场景会随着完成一个步骤而发生改变,需要不断更新,而每一个步骤的函数或代码块,都只需要考虑固定场景的输入,写起来简单很多。以下附上依照此完整思路的程序代码,代码量看上去依然不少,但程序语义清晰简洁,比代码量少更重要。
#include <iostream>
using namespace std;
//此函数负责合并数组符号相同且相邻的数
void first_merge(int a[], int& n)
{
int i=0, length=n;
n = 0;
while((i+1)<length)
{
if(a[i]*a[i+1]>=0)
{
a[i+1] += a[i];
}
else
{
a[n] = a[i];
n += 1;
}
i += 1;
}
a[n] = a[i];
n += 1;
}
//这里已经确保传入的数组不止一个元素了,函数写起来便可以不考虑单元素数组
void trim(int** a, int& n)
{
int *arr = *a;
if(arr[n-1] < 0)
{
n -= 1;
}
if(arr[0] < 0)
{
*a += 1;
n -= 1;
}
}
//按规则合并三个数,记录连续最大和,返回合并值
//参数一定是:n1为正数,n2为负数,n3为正数
int tri_nums_merge(int n1, int n2, int n3, int& maxsum)
{
int sum = n1+n2+n3;
maxsum = n1 > n3 ? n1:n3;
maxsum = maxsum > sum ? maxsum:sum;
int merge_value = n1+n2 > 0 ? sum:n3;
return merge_value;
}
//求连续子数组最大和
int max_sum(int* a,int n)
{
int maxsum = a[0];
//判断是否全为负数,若是,返回
int i = 0;
for(i=0; i<n; i++)
{
if(a[i]>0)
break;
else
maxsum = maxsum < a[i] ? a[i]:maxsum;
}
if(i == n)
return maxsum;
first_merge(a, n);
trim(&a, n);
//此时如果数组只剩一个数,返回;否则按规则迭代合并
if(n == 1)
return a[0];
else
{
int n1 = a[0];
maxsum = 0;
for(int i=0; i<n-2; i+=2)
{
n1 = tri_nums_merge(n1, a[i+1], a[i+2], maxsum);
}
return maxsum;
}
}
int main()
{
int a1[3] = {-1, -2, -3};
int a2[3] = {1, 2, 3};
int a3[6] = {-2, -1, 3, -2, 3, -1};
int a4[6] = {2, -3, 4, 5, -4, 6};
cout << "a1:" << max_sum(a1, 3) << endl;
cout << "a2:" << max_sum(a2, 3) << endl;
cout << "a3:" << max_sum(a3, 6) << endl;
cout << "a4:" << max_sum(a4, 6) << endl;
return 0;
}