最大子数组问题
假如我们能获得股票在17天内的走势,老板要我们规划一下买入和卖出的时间来获得利益的最大化,那通过这个走势图我们如何确定最大收益下的买入和卖出时间点呢?
通过股价走势图,我们可以看出,在第7天时股价最低,在第1天股价最高,按照通常的逻辑,我们应该在股价最低时买入,在股价最高时卖出,这样就可以获得利益的最大化,但问题来了,股价最低的第7天是在股价最高的第1天后面的,我们不可能让时光倒流,在第7天买入股票,在第1天卖出股票,因此这个最大化收益问题是有条件限制的,也就是卖出的时间必须在买入时间之后。
那求解方法就有以下两种:
- 暴力求解法
暴力求解法就是尝试将每对买入和卖出的日期进行组合,找到其中的最大值,只要卖出日期在买入日期之后即可。这样的方法需要设置两个循环遍历整个数组,因此时间复杂度为O(n^2),这样的速度太慢,不可取。 - 分治策略求解法
我们可以换一种思维,寻找股价收益最大的日期范围也就相当于寻找股价变化最大的日期范围,而我们将数据变换一下,将输入数据从股价变换成当天股价和昨天股价的差,这样就形成了一个长度为16的数组,接下来的问题就转换成了寻找该数组的和最大的子数组。
股价变化:
| 13 | -3 | -25 | 20 | -3 | -16 | -23 | 18 | 20 | -7 | 13 | 12 | -5 | -22 | 13 | 15 | -4 | 7 |
通过表格,我们可以很明显看出8~11的子数组是最大的,也就是说我们在第8天买入,在第11天卖出的利润是最高的,总共可获得每股43元的收益。
而通过分治策略来进行求解最大子数组时,我们可以将数组划分为两部分,从中间划开,分为左部分和右部分,那最大子数组就只可能是以下三种情况:
- 最大子数组在左部分
- 最大子数组在右部分
- 最大子数组在左右交界的部分
而我们可以很容易的在线性时间内找出跨中点的最大子数组
伪代码如下:
FIND-MAX-CROSSING-SUNARRAY(A,low,mid,high)
left-sum=-∞
sum=0
for i = mid downto low
sum=sum+A[i]
if sum>left-sum
left-sum=sum
max-left=i
right-sum=-∞
sum=0
for j= mid +1 to high
sum=sum+A[i]
if sum>right-sum
right-sum=sum
max-right=j
return (max-left,max-right,left-sum+right-sum)
该算法用于求解跨中点的最大子数组,通过从中点mid出发向两边移动,分别求出左半部分和右半部分的最大子数组,然后再进行合并,便得到了跨中点的最大子数组。其中最大子数组的寻找采用的是累加法,使用sum变量来对数组进行累加,用left-sum来记录左半部分最大子数组的值,用max-left来记录左半部分最大子数组的左端下标,若数组累加的结果大于最大子数组的值,则进行替换。
跨中点的最大子数组的算法有了之后,我们便可以求解整体数组下的最大子数组了,总体算法为:
- 求出左半的最大子数组(递归)
- 求出右半的最大子数组(递归)
- 求出跨中点的最大子数组
- 返回三者间的最大者
通过不断的递归最终可以求出整体数组下的最大子数组
C语言代码如下:
#include<stdio.h>
#define min -99999999
typedef int ElemType;//元素类型
typedef struct Result{
int max_left;//最大子数组左端下标
int max_right;//最大子数组右端下标
ElemType val;//最大子数组值
}Result;
Result Find_Max_Crossing_Subarray(ElemType A[],int low,int mid,int high);//计算分治算法下求中间的最大子数组
Result Find_Maximum_SubArray(ElemType A[],int low,int high);//递归求出总数组下的最大子数组
void Print_Result(Result result);//输出最大子数组的值
Result Create_Result(int x,int y,ElemType z);
void main()
{
int low=0,high=16;
ElemType A[]={13,-3,-25,20,-3,-16,-23,18,20,-7,12,-5,-22,15,-4,7};
Result result=Find_Maximum_SubArray(A,low,high);
Print_Result(result);
}
Result Find_Max_Crossing_Subarray(ElemType A[],int low,int mid,int high)//计算分治算法下求中间的最大子数组
{
int max_left,max_right;//最左端下标,最右端下标
ElemType left_sum=min,right_sum=min;//左端设为最小值 右端设为最小值
//求左边最大
ElemType sum=0;//左端和
for(int i=mid;i>=low;i--)//从中间值循环到最左端
{
int index=i-1;
sum+=A[index];//累加求和
if(sum>left_sum)//若累加后的值比原最大值大,则进行替换
{
left_sum=sum;
max_left=i;
}
}
//求右边最大
sum=0;
for(int j=mid+1;j<=high;j++)
{
int index=j-1;
sum+=A[index];//累加求和
if(sum>right_sum)//若累加后的值比原最大值大,则进行替换
{
right_sum=sum;
max_right=j;
}
}
return Create_Result(max_left,max_right,left_sum+right_sum);//返回左端下标、右端下标和两部分的值之和
}
Result Find_Maximum_SubArray(ElemType A[],int low,int high)//递归求出总数组下的最大子数组
{
if(high==low)//若数组只有一个元素,则返回该元素
return Create_Result(low,high,A[low-1]);
else
{
int mid=(low+high)/2;//获取中间下标
Result left_result=Find_Maximum_SubArray(A,low,mid);//递归获取左半部分最大子数组
Result right_result=Find_Maximum_SubArray(A,mid+1,high);//递归获取右半部分最大子数组
Result cross_result=Find_Max_Crossing_Subarray(A,low,mid,high);//获取中间部分最大子数组
//返回值最大的子数组
if(left_result.val>=right_result.val&&left_result.val>=cross_result.val)
return left_result;
else if(right_result.val>=left_result.val&&right_result.val>=cross_result.val)
return right_result;
else
return cross_result;
}
}
Result Create_Result(int x,int y,ElemType z)
{
Result result;
result.max_left=x;
result.max_right=y;
result.val=z;
return result;
}
void Print_Result(Result result)//输出最大子数组的值
{
printf("left: %d\t right: %d\t val: %d\n",result.max_left,result.max_right,result.val);
}