连续子数组的最大和问题的五种解法

问题描述

  一个整数数组中的元素有正有负,在该数组中找出一 个连续子数组,要求该连续子数组中各元素的和最大,这个连续子数组便被称作最大连续子数组。比如数组{2,4,-7,5,2,-1,2,-4,3}的最大连续子数组为{5,2,-1,2},最大连续子数组的和为5+2-1+2=8。问题输入就是一个数组,输出该数组的“连续子数组的最大和”。

五种解法

(1)暴力算法  三层循环,O(n^3)级别

//2, 4, -7, 5, 2, -1, 2, -4, 3 
//改良版1  暴力模拟,三层循环,O(n^3)级别
//任何一个子数组都能遍历到,都判断一次,即便是只有一个。
int MaxLong1(int a[], int len)
{
    //比较MaxLong2,仅仅是加了一层控制,而这个控制可以不要
    int i, j, k;
    int Maxnum = 0;
    int Curnum = 0;
    for (i = 0; i < len; i++)  //确保每个数字都能遍历到
    {
        for (j = i; j < len; j++)  //控制某子数组内1内
        {
            Curnum = 0;
            for (k = i; k <= j; k++) //控制某子数组内1内的某子数组2内,寻找最大值
            {
                Curnum += a[k];
                if (Curnum>Maxnum)
                {
                    Maxnum = Curnum;
                }
            }
        }
    }
    return Maxnum;
}

(2)在(1)暴力算法基础上撤出一个for循环,O(N^2)级别

//改良版2  在改良版1基础上撤出一个for循环,O(N^2)级别
//2, 4, -7, 5, 2, -1, 2, -4, 3 
//复杂度 O(N*2)
//任何一个子数组都能遍历到,都判断一次,即便是只有一个。
int MaxLong2(int a[], int len)
{
    //比较MaxLong1,仅仅是省去了一层控制----第三个for,
    //而这个控制可以不要,可以根据for循环的特性去掉,因为for循环本来就是这样子下去的,这样就重复计算了前面计算的,增加了复杂度
    int i, j;
    int Maxnum = 0;
    int Curnum = 0;
    for (i = 0; i < len; i++)       //控制每个数字都能遍历到
    {
        Curnum = 0;
        for (j = i; j < len; j++)   //控制在子数组1内,同时,还能控制子数组1内任意连续子数组2内都能判定一次
                                    //利用的是for循环++的特性。
        {
            Curnum += a[j];
            if (Curnum > Maxnum)
            {
                Maxnum = Curnum;
            }
        }
    }
    return Maxnum;
}

(3)分治法  次优算法,采用分治策略  时间复杂度T(n)= O(nlogn)

因为最大子序列和可能在三处出现,整个出现在数组左半部,或者整个出现在右半部,又或者跨越中间,占据左右两半部分(向两边搜索前进)。递归将左右子数组再分别分成两个数组,直到子数组中只含有一个元素,退出每层递归前,返回上面三种情况中的最大值。

int Divide(int a[], int left, int right)
{
    int leftMaxSum, rightMaxSum;  //左边最大值  右边最大值
    int midleftMaxSum,midrightMaxSum;  //中间的左边最大值,中间的右边最大值
    int midleft , midright ; //左边中间项,右边中间项
    if (left == right)
    {
        if (a[left] > 0)
        {
            return a[left];
        }
        else
        {
            return 0;
        }
    }
    int center = (left + right) / 2;
    leftMaxSum = Divide(a, left, center);  //左边  //这里不能-1,因为(left + right) / 2向下取证整了
                                                   //例如 只剩两个数,center = 0,center -1 = -1,就会报错了;
    rightMaxSum = Divide(a, center + 1, right); //右边   //这里加一不会出错,因为已经向下取证了  
    //中间
    //中间的左边
    midleftMaxSum = 0;
    midleft = 0;
    for (int i = center; i >= left; i--)
    {
        midleft += a[i];
        if (midleft > midleftMaxSum)
        {
            midleftMaxSum = midleft;
        }
    }
    //中间的右边
    midrightMaxSum = 0;
    midright = 0;
    for (int i = center+1; i <= right; i++)
    {
        midright += a[i];
        if (midright > midrightMaxSum)
        {
            midrightMaxSum = midright;
        }
    }
    int midmax = midleftMaxSum + midrightMaxSum; //中间最大值
    //比较出最大值
    int max = leftMaxSum > rightMaxSum ? leftMaxSum : rightMaxSum;
    return max > midmax ? max : midmax;
}

(4)动态规划(DP)   复杂度为 O(n)。

  步骤 1:令状态 dp[i] 表示以 A[i] 作为末尾的连续序列的最大和(这里是说 A[i] 必须作为连续序列的末尾)。

  步骤 2:做如下考虑:因为 dp[i] 要求是必须以 A[i] 结尾的连续序列,那么只有两种情况:

    1.  这个最大和的连续序列只有一个元素,即以 A[i] 开始,以 A[i] 结尾。---》(DP[i-1]小于0)
    2.  这个最大和的连续序列有多个元素,即从前面某处 A[p] 开始 (p<i),一直到 A[i] 结尾。  --》(DP[i-1]>0)

  对第一种情况,最大和就是 A[i] 本身。

  对第二种情况,最大和是 dp[i-1]+A[i]。

  于是得到状态转移方程

        dp[i] = max{A[i], dp[i-1]+A[i]}

  这个式子只和 i 与 i 之前的元素有关,且边界为 dp[0] = A[0],由此从小到大枚举 i,即可得到整个 dp 数组。接着输出 dp[0],dp[1],...,dp[n-1] 中的最大子即为最大连续子序列的和。

#include <iostream>
using namespace std;
#include <algorithm>
#include <functional>

//动态规划
int max(int a, int b)
{
    return a > b ? a : b;
}
//   dp[i] = max{A[i], dp[i-1]+A[i]}
void process(int a[], int dp[], int size)  //求出各个节点为结尾的最大值
{
    for (int i = 1; i < size; i++)
    {
        dp[i] = max(a[i], a[i] + dp[i - 1]);
    }
}

void main()
{
    int a[] = { 2, 4, -7, 5, 2, -1, 2, -4, 3 };
    int size = sizeof(a) / sizeof(int);
    int DP[100] = { 0 };          //假设100够用了
    process(a, DP, size);
    sort(DP, DP + size, greater<>());  //直接打乱,反正也不用了,当然可以if判断,我懒一点
    cout << "最大连续子序列: " << DP[0] << endl;
    system("pause");
}

(5)线性时间算法   时间复杂度O(n)

    该算法在每次元素累加和小于0时,从下一个元素重新开始累加。实现代码如下:

//最优方法,时间复杂度O(n)

int line(int a[], int size)
{
    int Maxnum = 0;
    int Curnum = 0;
    for (int i = 0; i < size; i++)
    {
        Curnum += a[i];
        if (Curnum > Maxnum)
        {
            Maxnum = Curnum;
        }
        //如果累加和出现小于0的情况,
        //则和最大的子序列肯定不可能包含前面的元素,
        //这时将累加和置0,从下个元素重新开始累加
        else if (Curnum < 0)  //为什么加上else ,因为if (Curnum > Maxnum)成立时,Curnum肯定大于0,没必要判断
        {
            Curnum = 0;
        }
    }
    return Maxnum;
}

(4)(5)两种算法应该是一种。

参考  https://www.cnblogs.com/allzy/p/5162815.html

         https://www.cnblogs.com/coderJiebao/p/Algorithmofnotes27.html

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值