编程之美4:那些常被考到的关于数组的最大子数组问题

楼主这篇文章的目的是要带大家梳理一下,有关于求子数组问题。如求子数组的最大和,求最大和的子数组,求最大积的子数组等一系列问题。今天阳光明媚,楼主今天心情很好哦,愿大家开心每一天,哈哈。Are you ready?开始了哦~~~

题目一:求子数组的最大和

题目求子数组的最大和,这里需要注意的一个问题就是,子数组那么便意味着是连续的一段数据。我们可以先写的例子,方便我们注意到要考虑的一些问题。

数组:[1, -2, 3, 5, -3, 2] 应该返回8,最大和的子数组为3,5
数组:[0, -2, 3, 5, -1, 2] 应该返回9,最大和的子数组为3, 5,-1,2
数组:[-1, -2, -3, -4, -5]应该返回-1,最大和的子数组为-1
这里,我们只介绍一种时间复杂度为 O(N) 的算法来求解。

解法一:分治法

将所给数组 (A[0],A[1],...,A[N1]) 分成两组, (A[0],A[1],...,A[N21]) (A[N2],A[N2+1],...,A[N1]) ,分别求出这两段数组各自的最大子段和,则原数组的子数组的最大和可以分为下面三种情况:

  • (A[0],A[1],...,A[N21]) 相同
  • (A[N2],A[N2+1],...,A[N1]) 相同
  • 跨过中间两个元素 A[N21]) A[N2])

看下面代码:

#include <iostream>  
#include <limits>

using namespace std;  

int getMAX3(int a, int b, int c);
int getBiggestSubSum(int *pArray, int low, int high);

int main()  
{  
    int a1[] = {1, -2, 3, 5, -3, 2};
    int a2[] = {1, -2, 3, 5, -1, 2};
    int a3[] = {-1, -2, -3, -5, -3, -2};

    cout << "数组1的子数子的最大和为" << getBiggestSubSum(a1, 0, sizeof(a1) / sizeof(int) - 1) << endl;
    cout << "数组2的子数子的最大和为" << getBiggestSubSum(a2, 0, sizeof(a2) / sizeof(int) - 1) << endl;
    cout << "数组3的子数子的最大和为" << getBiggestSubSum(a3, 0, sizeof(a3) / sizeof(int) - 1) << endl;
    system("pause");
}  

int getBiggestSubSum(int *pArray, int low, int high)
{
    int middle = (low + high) >> 1; //获取中间节点的下标
    int MAX = numeric_limits<int>::min();
    if (high == low)
    {
        //递归到只剩下一个数之后,那么直接返回
        return pArray[low];
    }

    int lMax = numeric_limits<int>::min(); 
    int rMax = numeric_limits<int>::min(); 
    int mMax = numeric_limits<int>::min(); 
    int mMaxTemp = 0;

    //求左半部分的最大和
    lMax = getBiggestSubSum(pArray, low, middle);
    //求右半部分的最大和
    rMax = getBiggestSubSum(pArray, middle + 1, high);

    //求横跨中间的最大和
    for (int i = middle; i <= high; i++)
    {
        mMaxTemp += pArray[i];
        mMax = max(mMax, mMaxTemp);
    }

    for (int i = middle - 1; i >= 0; i--)
    {
        mMaxTemp += pArray[i];
        mMax = max(mMax, mMaxTemp);
    }

    //返回三大部分最大和之间的最大值
    return getMAX3(rMax, mMax, lMax);
}

int getMAX3(int a, int b, int c)
{
    int max = a;
    max = (b > max) ? (b):(max);
    max = (c > max) ? (c):(max);
    return max;
}

算法复杂度: O(NlogN)

解法二:动态规划法

我们可以考虑数组的第一个元素 A[0] ,以及最大的一段数组 (A[i],...,A[j]) 跟$A[0]的关系,有以下几种情况:

  • i = 0 = j,这便意味着,最大和的子数组只由一个 A[0] 组成;
  • 0 = i < j,这便意味着,最大和的子数组是从 A[0] 开始的一串序列组成;
  • 0 < i < j,这便意味着,最大和的子数组和 A[0] 无关,不是从 A[0] 开始的。

从上面三种情况可以看出来,我们可以将一个大问题,转化成一个较小的问题。如本题中,我们可以将一个N维数组的子数组最大和问题,转化成N-1维的子数组最大和问题。假设已经知道 (A[1],...,A[N1]) 子数组的最大和为 All[1] ,还知道包含 A[1] 的子数组的最大和为 Start[1] 。那么根据上面的分析可知

All[0]=max{A[0],A[0]+Start[0],All[1]}

上述公式中,第一项表示只有一个 A[0] 元素构成最大和子数组,第二项表示 A[0] 和从 A[1] 开始的序列构成最大和子数组,第三项表示,最大和子数组和 A[0] 无关。看下面代码:

#include <iostream>  

using namespace std;  

int getBiggestSubSum(int *pArray, int len);

int main()  
{  
    int a1[] = {1, -2, 3, 5, -3, 2};
    int a2[] = {1, -2, 3, 5, -1, 2};
    int a3[] = {-1, -2, -3, -5, -3, -2};

    cout << "数组1的子数子的最大和为" << getBiggestSubSum(a1, sizeof(a1) / sizeof(int)) << endl;
    cout << "数组2的子数子的最大和为" << getBiggestSubSum(a2, sizeof(a2) / sizeof(int)) << endl;
    cout << "数组3的子数子的最大和为" << getBiggestSubSum(a3, sizeof(a3) / sizeof(int)) << endl;
    system("pause");
}  

int getBiggestSubSum(int *pArray, int len)
{
    //从数组的最后一个元素开始,当只有一个元素的时候,那么start和all当然都是等于这个数啦
    int nStart = pArray[len - 1];
    int nAll = pArray[len - 1];

    for (int i = len - 2; i >= 0; i--)
    {
        nStart = max(pArray[i], nStart + pArray[i]);
        nAll = max(nAll, nStart);
    }
    return nAll;
}

运行结果:

数组1的子数子的最大和为8
数组2的子数子的最大和为9
数组3的子数子的最大和为-1
请按任意键继续. . .

算法复杂度: O(NlogN)

题目二:求子数组的最大和并输出相应的子数组

题目一只管求出子数组的最大和,不管子数组是什么。而这个题目中,我们需要求解子数组序列的起始位置和结束位置。其实和上面差不多,只是需要记录位置而已,看代码,在题目一的解法二上面修改的。

#include <iostream>  

using namespace std;  

int getBiggestSubSum(int *pArray, int len, int *startIndex, int *endIndex);
void printMaxSubArray(int *pArray, int start, int end);

int main()  
{  
    int a1[] = {1, -2, 3, 5, -3, 2};
    int a2[] = {1, -2, 3, 5, -1, 2};
    int a3[] = {-1, -2, -3, -5, -3, -2};

    int start = 0;
    int end = 0;

    cout << "数组1的子数子的最大和为" << getBiggestSubSum(a1, sizeof(a1) / sizeof(int), &start, &end) << endl;
    cout << "和最大的子序列为" << endl;
    printMaxSubArray(a1, start, end);

    cout << "数组2的子数子的最大和为" << getBiggestSubSum(a2, sizeof(a2) / sizeof(int), &start, &end) << endl;
    cout << "和最大的子序列为" << endl;
    printMaxSubArray(a2, start, end);
    cout << "数组3的子数子的最大和为" << getBiggestSubSum(a3, sizeof(a3) / sizeof(int), &start, &end) << endl;
    cout << "和最大的子序列为" << endl;
    printMaxSubArray(a3, start, end);
    system("pause");
} 


int getBiggestSubSum(int *pArray, int len, int *startIndex, int *endIndex)
{
    //从数组的最后一个元素开始,当只有一个元素的时候,那么start和all当然都是等于这个数啦
    int nStart = pArray[len - 1];
    int nAll = pArray[len - 1];
    *startIndex = len - 1;
    *endIndex = len - 1;

    for (int i = len - 2; i >= 0; i--)
    {
        if (pArray[i] > nStart + pArray[i]) 
        {       
            nStart = pArray[i];
            if (nStart > nAll)
            {
                //如果仅仅包含pArray[i]的子序列获得最大和的话,那么startIndex和endIndex和nAll都需要更新
                *startIndex = i;
                *endIndex = i;
                nAll = pArray[i];
            }
            else
            {
                //还是原来的子序列和最大,那startIndex和endIndex都不变了
            }
        }
        else
        {
            nStart = nStart + pArray[i];
            if (nStart > nAll)
            {
                //如果包含a[i],a[i+1],...a[j]获得最大的和的话,那么要更新startIndex和nAll
                nAll = nStart;
                *startIndex = i;
            }
            else
            {
                //还是原来的子序列和最大,那startIndex和endIndex都不变了
            }
        }
    }
    return nAll;
}

void printMaxSubArray(int *pArray, int start, int end)
{
    for (int i = start; i <= end; i++)
    {
        cout << pArray[i] << " ";
    }
    cout << endl;
}

时间复杂度同样是 O(N)

题目三:求子数组的最大积

这里必须要重述一下题目,和题目一以及题目二是不一样的!!!!

给定一个长度为N的整数数组,只允许用乘法,不能用除法,计算任意N-1个数的组合乘积中的最大的一组,并写出算法的时间复杂度。

这里的子数组,是任意N-1个数的组合,并不一定是连续的N-1的子序列。

解法一

其实这道题,腾讯某年笔试题的加试题就有这道题,感兴趣的同学可以看这个链接2012年腾讯实习生笔试附加题
题目简介:已知数组a[n],求数组b[n].要求:b[i]=a[0]a[1]……*a[n-1]/a[i],不能用除法。a.时间复杂度O(n),空间复杂度O(1)。 b.除了迭代器i,不允许使用任何其它变量(包括栈临时变量等),看,是不是差不多?

这里我们同样可以使用前累乘后累乘的办法把所有的b[i]求出来,然后遍历一遍,求出最大的积。看代码:

#include <iostream>  

using namespace std;  

void getBiggestMultipleResults(int *pArray, int *pResultsArray, int len);

int main()  
{  
    int a[] = {1, 2, 3, 4};
    int len = sizeof(a) / sizeof(int);
    int *b = new int[len]();
    int result = b[0];

    //求出所有的b[i]
    getBiggestMultipleResults(a, b, len);

    for (int i = 0; i < len; i++)
    {
        result = max(b[i], result);
    }

    cout << result << endl;

    delete []b;
    system("pause");
} 


void getBiggestMultipleResults(int *pArray, int *pResultsArray, int len)
{
    pResultsArray[0] = 1;
    //前累乘
    for (int i = 1; i < len; i++)
    {
        pResultsArray[i] = pResultsArray[i - 1] * pArray[i - 1];
    }

    //后累乘
    for (int i = len - 2; i > 0; i--)
    {
        //把pResultsArray[0]作为临时变量
        pResultsArray[0] *= pArray[i + 1];
        pResultsArray[i] *= pResultsArray[0];
    }

    pResultsArray[0] *= pArray[1];
}

解法二:数学分析法

看完这个数学分析的方法,不禁感叹,还是要数学好啊!不信,你看吧。

假设N个证书的乘积为P, 针对P的正负性进行分析:

1. P=0

那么,数组中至少有1个0。除去这个0之外,其他N-1个数的乘积为Q,根据Q的正负性进行分析:

  • Q = 0

结果返回0.
分析:Q=0说明Q数组里面还有1个0,加上前面P数组中的1个0,我们可以断定,任意N-1个数的乘积只能为0,那么结果返回0.

  • Q > 0

结果返回Q
分析:如果以0替换此时Q数组里面任意一个数的话,那么结果都是0,所以最后结果为Q

  • Q < 0

结果返回0
分析:如果以0替换此时Q数组里面的任意一个,则结果为0,0比负数大,所以结果返回0

2. P > 0

从数组中去除一个绝对值最小的正整数,这样得到剩下的 PN1 就是最大的。

3. P< 0

从数组中去除一个绝对值最小的负整数,剩下的 PN1 就是最大的。

这样的解法,你服不服?反正我服了。

还有一些题目没有时间更新了,因为要汇报啊。预告一下,这个还会有更新那个什么,“最大递增子序列”,“最大的group个数”,后续会更新上来。今天就到这儿位置喽,祝大家晚上愉快,good night哦。(~ o ~)~zZ

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值