求最大子数组及其优化——互联网公司常见面试题

最大子数组算法

最大子数组算法是一个很经典的算法问题,据说是某些主流互联网公司面试常常会提问的问题。

问题描述

给定一个整数数组,要求找出元素之和最大的子数组。即给你一个数组Arr[a1,a2,a3…,an],求下标j,k,使得sum = aj+a(j+1)+a(j+2)+…+ak为最大值。

(最大子数组问题不仅仅是测试一个有潜力的雇员思考能力的很好的问题,而且该问题也常常应用于数字化图像处理、模式识别)

注意,数组的元素可以是正数、负数、0,因此在数组的所有元素都是负数的特殊情况下,问题的解是空子数组,通常认为其和是0,因此设置arr[0] = 0

方法解决

方法一:暴力穷举法

最容易想到的方法肯定就是将该数组的所有子数组都找出来,然后求和,最后判断最大的子数组。

原理很简单就不细讲了,奉上代码:

其中ElemType为自定义数据类型,为int整型。

ElemType GetMaxSubarray(ElemType arr[],int len)
{
    ElemType max = 0;
    ElemType sum = 0;
    for(int j=1;j<=len;j++)//从1开始的所有子数组
    {
        for(int k=j;k<=len;k++)//从j开始的所有长度逐渐变长的数组
        {
            for(int i=j;i<=k;i++)//求和
            {
                sum +=arr[i];
            }
            if(sum > max)
            {
                max = sum;
            }
            sum = 0;
        }
    }
    return max;
}
int main()
{
    ElemType arr[12]={0,-2,-4,3,-1,5,6,-7,-2,4,-3,2};
    int max = GetMaxSubarray(arr,11);
    printf("%d ",max);
    getchar();
    return 0;
}

测试输出结果为:13 ,结果正确。

但是到这里就结束了嘛?当然不会,这种方法的时间复杂度太高,为O(n^3)。如果你告诉面试官你的思路是如上思路,可能你就已经被踢出群聊了 T T

下面我们就来一步步优化,看看有什么方法能降低时间复杂度^ ^

优化一:利用前缀

上面的第一种方法是从头开始计算所有子数组的和,浪费了很多时间。因此我们每次计算子数组和的时候可以利用之前计算的前缀和,即前t个元素的和。原理就是sum(j,k) = S(k)-S(j-1),S(k)为前k项元素的和,相应的,也就需要多付出些存储空间来存储S(k)

ElemType GetMaxSubarray_Faster(ElemType arr[],int len)
{
    ElemType max = 0;
    ElemType *S = (ElemType *)malloc(sizeof(ElemType)* (len+1));//用来存储前K项和
    S[0] = 0;
    for(int i=1;i<=len;i++)//计算前缀和
    {
        S[i]=S[i-1]+arr[i];
    }
    for(int i=1;i<=len;i++)//双层循环求其所有子数组
    {
        for(int j=i;j<=len;j++)
        {
            if(S[j] - S[i] > max)
            {
                max = S[j] - S[i];
            }
        }
    }
    free(S);
    return max;
}

可以看出,现在的时间复杂度已经优化到了O(n^2)。当你带着如上的思路去回复面试官,面试官可能就会心想:“可以,没有掉入使用第一种方法的陷阱”。但是这也仅仅是没有调入“陷阱”,而接下来面试官可能就会向你介绍一下更深一层的优化,将它优化到O(n)

优化二:最大后缀

现在,我们改为计算最大后缀来解决这个问题。定义M(t)为最大后缀和,使M(t) 为s(j,k) (j=1,2,3,…,t)中的最大者。

这种定义方法很有趣,但是也有缺陷,因为它不包括边界的情况,即所有结束于t的子数组之和为负数的情况,那么此时不考虑任何结束于t的子数组。为此,定义M(t) = max{0, max(j=1…t){s(j,t)}}。也就是说,M(t)是0与最大和s(j,t)中较大的值。

上面的定义说明,如果M(t) >0,那么它是一个结束于t的某个最大子数组的和,如果M(t)=0,那么可以安全的忽略任何在t结束的子数组。

那么如何计算M(t)呢? ?_ ?

注意到对于t≥2时,如果有一个结束于t的最大子数组,而且其和是正的,那么这个最大子数组或者是A[t:t],或者是由结束于t-1的最大子数组加上A[t]组成的。此外,如果结束于t-1的最大子数组之和加上A[t]小于0,那么M(t) = 0,因为不存在以t结尾而且其和为正数的子数组。

换句话说,可以定义M(0) = 0为边界条件,且M(t) = max{0,M(t-1)+A[t]}
在这里插入图片描述

ElemType GetMaxSubarray_Fastest(ElemType arr[],int len)
{
    ElemType max =0 ;
    ElemType *M = (ElemType *)malloc(sizeof(ElemType)*(len+1));
    M[0] = 0;
    for(int i=1;i<=len;i++)
    {
        if(M[i-1] + arr[i] > 0)
        {
            M[i] = M[i-1] + arr[i];
        }else
        {
            M[i] = 0;
        }
    }
    for(int j=1;j<=len;j++)
    {
        if(M[j] > max)
        {
            max = M[j];
        }
    }
    free(M);
    return max;
}

可以看出,时间复杂度已经被优化为O(n)了!这就是思考的魅力!

虽然我们不能保证你回答了这种方式就会通过面试^ _ ^,但至少可以保证这是回答这个问题一个比较好的方式。

猜你喜欢

转载自blog.csdn.net/qq_42642142/article/details/107286781