最大子数组和(连续子数组的最大和)—— 提高篇

最大子数组和

此题解法仅为为个人理解,如有其他思路或解法欢迎来探讨!
基础篇链接:最大子数组和(连续子数组的最大和)—— 基础篇


题目:

已知一个整数数组 arr,找到子数组的最大和(子数组最少包含一个元素)并返回这个子数组。
子数组元素顺序不发生改变且需连续。


示例:

输入: arr = [-2,1,-3,4,-1,2,1,-5,4]
输出: sum = 6 ,nums = [4,-1,2,1] 

分析:

首先这道题是我们老师给我们出的,要求我们使用分治算法来写。所以接下来的分析都是围绕着分治算法来进行解析:

在计算机科学中,分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。
比如:排序算法中的快速排序与归并排序,傅里叶变换等

这里就要来说明一下分治法适用的情况了:

  1. 该问题的规模缩小到一定的程度就可以容易地解决;
  2. 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质;
  3. 利用该问题分解出的子问题的解可以合并为该问题的解;
  4. 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。

第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;

第二条特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用;、

第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法。

第四条特征涉及到分治法的效率,如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好

现在

将视线回到这道题上,按照分治算法,我们要将数组从中间拆分,使其成为更小的问题。在方法maxSubArray中,我们需要的元素有——所输入的数组、指向左子组的下标left、指向右子组的下标right、指向中间分隔值的下标mid。用递归实现,当子数组中只剩一位时返回其本身if (left == right) return nums[left];

这时,会出现三种情况,最大和出现在左子组、最大和出现在右子组、最大和出现在跨两个子组的位置。这里左子组的从left—mid、右子组是从mid+1—right、而跨左右数组的最大子数组和,则需要单独再写一个方法midSubArray来计算了。

在跨左右数组的最大子数组和的计算中,我们要分左右进行计算和的计算,让指针从中值mid开始,向两边进行移动就能保证始终是以最大和来计算的,当和sum加上指针所在的那一位时的所得值比之前的sum更小,则不将此次的和存入leftSum(rightSum)。这样将左右子组都计算完毕时,返回leftSum + rightSum就是跨左右数组的最大子数组和。
注意:这里在定义leftSumrightSum时要将值设置为极小值而不能为0,因为输入的数组为整数,要考虑子数组中出现负数的情况。且不论是最大和产生在左子组还是右子组,都会进这一步,所以不用再专门写左子组合右子组的求和。

int leftSum = Integer.MIN_VALUE;//定一个极小值给左数组和
int rightSum = Integer.MIN_VALUE;//定一个极小值给右数组和

midSubArray方法返回值回到maxSubArray时,求出当前左子组最大和、右子组最大和、跨左右子组最大和,返回其中的最大值就可以了。

return Math.max(leftSum,Math.max(rightSum,midSum));//返回较大的哪个和

最后,在递归的一层层返回后,就得到了我们所求的最大子数组和。




什么?
你说没写完?还有个返回这个子数组的要求没有实现。
嗯嗯,是的。这个还没有写。上面的其实是最大子数组和—— 基础篇 最后分治算法的解析,而提高篇和基础篇的区别就在,提高篇要找到并输出哪个产生最大和的子数组。这在我第一次接触到这道题时可烦到我了,当时我对分治算法还不是特别熟悉,我找不到哪个子数组的下标。但如今做出来后再看起分治算法,便有了更通透的理解。这里我变分享一下我的写法:

由上面分治算法的说明我们可以知道,分治算法是将一个大问题分成若干个小问题,有点像二叉树那样子。那我们想找到哪个子数组的头尾下标,就要从小问题着手。
我们先定义两个静态变量来记录头尾位置下标(改成其他的会更好,在这道题我这样更方便看到罢了)

public static int startIndex;//记录子数组的起始下标
public static int endIndex;//记录子数组的结束下标

那我们在哪里给这两个变量赋值呢?嗯,对了,就是在midSubArray方法中的每一次将所得最大和存入leftSum(rightSum)时写入下标i,因为每一次更新最大和,i指到那个对应的数就刚好是我们所需要的那个边界上的数,在这存入就会一直保证每次更新最大和时都能更新到下标。
最后再按照下标输出就好咯。

结束!


代码:

先看上面的分析!自己理解思考一下,不要急着看代码!

分值算法:

public class DivideAndConquer {

    public static int startIndex;//记录子数组的起始下标
    public static int endIndex;//记录子数组的结束下标

    @Test
    public void test(){
        int[] arr = {1, -2, 3, 10, -4, 7, 2, -5};
        System.out.println("输入数组:" + Arrays.toString(arr));
        System.out.println("和最大值为:" + maxSubArray(arr,0,arr.length-1));
        printSubArray(arr,startIndex,endIndex,maxSubArray(arr,0,arr.length-1));

        int[] arr1 = {-2, -1, -5, -9, -8};
        System.out.println("输入数组:" + Arrays.toString(arr1));
        System.out.println("和最大值为:" + maxSubArray(arr1,0,arr1.length-1));
        printSubArray(arr1,startIndex,endIndex,maxSubArray(arr1,0,arr1.length-1));
    }

    public static void printSubArray(int[] nums , int startIndex , int endIndex , int sum){

        if (sum < 0){
            System.out.println("和最大子数组为:[" + sum + "]");
            return;
        }

        System.out.print("和最大子数组为:[" + nums[startIndex]);
        while (startIndex < endIndex){
            System.out.print("," + nums[startIndex + 1]);
            startIndex++;
        }
        System.out.println("]");
    }

    public static int maxSubArray(int[] nums , int left , int right){
        if (left == right) return nums[left];//左下标 == 右下标时,这时为分到最小,仅有一个元素,返回即可
        int mid = (left + right) / 2;//取中值
        int leftSum = maxSubArray(nums , left , mid);//求左数组最大子数组和
        int rightSum = maxSubArray(nums , mid +1 , right);//求右数组最大子数组和
        int midSum = midSubArray(nums , left , mid , right);//求跨左右数组的最大子数组和

        return Math.max(leftSum,Math.max(rightSum,midSum));//返回较大的哪个和
    }

    public static int midSubArray(int[] nums , int left , int mid , int right){
        int leftSum = Integer.MIN_VALUE;//定一个极小值给左数组和
        int rightSum = Integer.MIN_VALUE;//定一个极小值给右数组和
        int sum = 0;

        //左数组,从中间往左加,得到左数组最大和
        for (int i = mid ; i >= left ; i--){
            sum += nums[i];
            //存入较大的和
            if (sum > leftSum){
                leftSum = sum;
                startIndex = i;//记录下标
            }
        }

        sum = 0;//清零sum值,开始右数组

        //右数组,从中间往右加,得到右数组最大和
        for (int i = mid + 1 ; i <= right ; i++){
            sum += nums[i];
            //存入较大的和
            if (sum > rightSum){
                rightSum = sum;
                endIndex = i;//记录下标
            }
        }

        return leftSum + rightSum;//返回左右最大和数,之和
    }
}

当然,这里还有一份代码,是我在写分治之前的一种写法,对我写分治算法有蛮大帮助的,也放在这里:

public class Solution {

    @Test
    public void test(){
        int[] arr = {1, -2, 3, 10, -4, 7, 2, -5};
        System.out.println("输入数组:" + Arrays.toString(arr));
        System.out.println("和最大值为:" + maxSubArray(arr));

        int[] arr1 = {-2, -1, -5, -9, -8};
        System.out.println("输入数组:" + Arrays.toString(arr1));
        System.out.println("和最大值为:" + maxSubArray(arr1));
    }

    public int maxSubArray(int[] nums) {
        if (nums.length < 1) return 0;
        else if (nums.length == 1) return nums[0];//简化

        int startIndex = 0;//记录子数组的起始下标
        int endIndex = 0;//记录子数组的结束下标
        int sum = 0;//存放和

        int temp = 0;//临时和,存放当前数组临时的和
        int tempStartIndex = -1;//临时起始下标

        for (int i = 0 ; i < nums.length ; i++){
            if (temp < 0)   //如果temp < 0;则要对temp重新赋值
            {
                temp = nums[i];    //对temp重新赋值
                tempStartIndex = i;  //暂时记录子数组(和最大)的新的起始位置(要看后续的sum 和 temp是否发生交换)
            }
            else
            {
                temp += nums[i];   //temp >= 0;算出当前的临时最大和
            }

            if (sum < temp) //如果此时 sum < temp;则表示此时的子数组和大于之前的子数组和
            {
                sum = temp; //将temp赋值给sum
                startIndex = tempStartIndex; //存入之前保存的临时起始下标,记录子数组的起始下标
                endIndex = i;   //记录子数组的结束下标
            }
        }

        if (endIndex - startIndex == 0){
            System.out.println("输入全为负数");
            return Arrays.stream(nums).max().getAsInt();//全为负数时,找到最大值
        }

        System.out.print("和最大子数组为:[");
        while (startIndex <= endIndex){
            System.out.print(nums[startIndex] + ",");
            startIndex++;
        }
        System.out.println("]");

        return sum;
    }
}

分治算法解释来自:

分治算法详解(超详细)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值