算法 - 求最大连续子数组和起始下标

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/red_sheeps/article/details/102767426

执行代码参见GitHub
https://github.com/GloryXu/algorithm/tree/master/src/main/java/com/redsun/algorithm/subarraysum

逛论坛无意中发现一个算法帖子,一时间也没想出更好的解决方式,搜索了下,整理下。
在这里插入图片描述

暴力破解

    @Override
    public void execute() {
        for (int i = 0; i < arr.length; i++) {
            // 遍历从i开始的所有子数组
            for (int j = i; j < arr.length; j++) {
                int thisSum = 0;

                // 累计求和
                for (int k = i; k <= j; k++) {
                    thisSum += arr[k];
                }
                if (thisSum > maxSum) {
                    // 记录最大子数组的位置和长度
                    maxSum = thisSum;
                    start = i;
                    end = j;
                }
            }
        }
    }

外面两层循环,里面一层循环求和,再进行比较,最后求出一个最大的子数组。
在求出最大子数组同时,记录下对应的start和end位置,即为最大子数组的对应下标。
该种算法的时间复杂度O(n^3)

分治法

代码块1

    private int findMaxChildArr(int[] arr, int left, int right) {
        if (left == right) {
            // 如果左等于右,则说明这个子数组只存在一个元素,则返回它用于比较
            return arr[left];
        } else {
            // 递归思想
            int middle = (left + right) / 2;
            // 寻找左侧数组最大值;
            int leftMax = findMaxChildArr(arr, left, middle);
            // 寻找右侧数组最大值;
            int rightMax = findMaxChildArr(arr, middle + 1, right);
            // 寻找中间最大值
            int middleMax = findMiddleMax(arr, left, right, middle);
            // 比较求出值
            if (leftMax >= rightMax && leftMax >= middleMax) {
                return leftMax;
            } else if (rightMax >= leftMax && rightMax > middleMax) {
                return rightMax;
            } else {
                return middleMax;
            }
        }

    }

代码块2

    public int findMiddleMax(int[] arr, int left, int right, int middle) {
        int lmidMax = -1000;
        int rmidMax = -1000;
        int sum = 0;
        // 找到向左的最大前缀
        for (int i = middle; i >= left; i--) {
            sum += arr[i];
            if (sum > lmidMax) {
                lmidMax = sum;
            }
        }
        // 找到向右的最大后缀
        sum = 0;
        for (int i = middle + 1; i <= right; i++) {
            sum += arr[i];
            if (sum > rmidMax) {
                rmidMax = sum;
            }
        }
        return lmidMax + rmidMax;
    }
  1. 将数组一分为二,那么该数组最大的子数组只可能有三种情况,位于左边,位于右边,位于中间(部分左边,部分右边)
  2. 那么就只要比较左边最大L1,右边最大R1,中间最大M1,得出的结果即是整个数组的最大子数组
  3. 在求左边最大L1(右边最大R1)的时候又回到了第一点,可再将左边(右边)的数组进行拆分再求对应左边最大L2(右边最大R2),依次递归最终left=right
  4. 横跨中间的最大值又是另一种求法,从middle—>leftmiddle—>right分别求最大,连起来即是最大,详见代码块2
    该算法的时间复杂度为O(N*LogN),个人理解:(二分法复杂度LogN)*(middle求最大值的N)
    该方法没想到怎么求解出对应最大子数组的下标,有会的童鞋指导下。

贪心算法

    @Override
    public void execute() {
        /**
         * sumStart ,sumEnd 分别表示累加数组的start位和end位
         * 也相当于 sumStart 是前一次计算结果,sumEnd 是后一次计算结果
         * 这里代码中潜在的关系是
         * sumEnd = sumStart + arr[end]
         */
        int sumStart = 0;
        int sumEnd = 0;
        int minStart = 0;// 表示最小的 minStart
        for (int endIndex = 0; endIndex < arr.length; endIndex++) {
             // 相当于构建累加数组 sumEnd 的过程,sumEnd[0..end] = sumEnd[0..end-1] + arr[end]
            sumEnd = sumEnd + arr[endIndex];
            // 寻找 minStart,minStart[j] = min(minStart[j-1],sum[0..j])
            if (sumStart < minStart) {
                minStart = sumStart;
                /**
                 * 如果此时sum[0..(endIndex - 1)](即sumStart) < minStart
                 * 则最大子数组的start只可能从endIndex开始
                 * 通俗一点就是endIndex(0~endIndex-1)之前的都已经被赋给minStart了
                 */
                this.start = endIndex;
            }
            // 更新 sumTotal
            if (sumEnd - minStart > maxSum) {
                maxSum = sumEnd - minStart;
                /**
                 * 如果maxSum值变动,说明最大子数组的end值为当前endIndex
                 */
                this.end = endIndex;
            }
            sumStart = sumStart + arr[endIndex];
        }
    }

在这里插入图片描述

  1. 因为是连续子数组,所以对于一个数组一定会存在end和start满足图片中的公式
  2. 所以最终演化成求解minStart和maxSum的两个,即是代码块中的两个判断的目的

该算法也是目前了解到的最优解,核心思想就是将用到了上一次循环的结果,不必每次都重新来过。代码中的注释已经很详细了,可以仔细体会。
只遍历了一次整个数组,时间复杂度O(N)

运行结果

最终运行的结果如下
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/red_sheeps/article/details/102767426