LeetCode算法题详解 53:最大子数组和

目录

  • [1. 问题描述](#1. 问题描述)
  • [2. 问题分析](#2. 问题分析)
    • [2.1 题目理解](#2.1 题目理解)
    • [2.2 核心洞察](#2.2 核心洞察)
    • [2.3 破题关键](#2.3 破题关键)
  • [3. 算法设计与实现](#3. 算法设计与实现)
    • [3.1 暴力枚举法](#3.1 暴力枚举法)
    • [3.2 动态规划(Kadane算法)](#3.2 动态规划(Kadane算法))
    • [3.3 分治法(进阶挑战)](#3.3 分治法(进阶挑战))
    • [3.4 前缀和优化](#3.4 前缀和优化)
  • [4. 性能对比](#4. 性能对比)
  • [5. 扩展与变体](#5. 扩展与变体)
    • [5.1 最大子数组乘积](#5.1 最大子数组乘积)
    • [5.2 环形子数组的最大和](#5.2 环形子数组的最大和)
    • [5.3 最大子矩阵和](#5.3 最大子矩阵和)
    • [5.4 带长度限制的最大子数组和](#5.4 带长度限制的最大子数组和)
  • [6. 总结](#6. 总结)
    • [6.1 核心知识总结](#6.1 核心知识总结)
    • [6.2 算法选择指南](#6.2 算法选择指南)
    • [6.3 应用场景](#6.3 应用场景)
    • [6.4 面试技巧](#6.4 面试技巧)

1. 问题描述

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。

示例 1:

复制代码
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6。

示例 2:

复制代码
输入:nums = [1]
输出:1

示例 3:

复制代码
输入:nums = [5,4,-1,7,8]
输出:23

提示:

  • 1 <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4

进阶: 如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的 分治法 求解。

2. 问题分析

2.1 题目理解

我们需要在数组中找到一个连续的子数组,使得该子数组的元素之和最大。注意子数组至少包含一个元素,且必须是连续的。

2.2 核心洞察

  • 连续性约束:子数组要求连续,这排除了选择不相邻元素的可能性
  • 负数影响:数组可能包含负数,这意味着不能简单地选择所有正数
  • 局部与全局:当前元素可以开始新的子数组,或加入之前的子数组

2.3 破题关键

问题的核心在于如何高效地决定每个元素是应该开始新的子数组还是加入现有的子数组。这引导我们思考两种主要思路:

  1. 动态规划:维护以每个位置结尾的最大子数组和
  2. 分治法:将问题分解为子问题,分别解决后合并

3. 算法设计与实现

3.1 暴力枚举法

核心思想

枚举所有可能的子数组,计算每个子数组的和,找出最大值。

算法思路

  1. 遍历所有可能的起始位置 i (0到n-1)
  2. 对于每个起始位置,遍历结束位置 j (i到n-1)
  3. 计算子数组 nums[i..j] 的和
  4. 记录所有和中的最大值

Java代码实现

java 复制代码
public class MaximumSubarrayBruteForce {
    /**
     * 暴力解法 - 三层循环
     * 时间复杂度: O(n³)
     * 空间复杂度: O(1)
     */
    public int maxSubArrayBruteForce1(int[] nums) {
        int n = nums.length;
        int maxSum = Integer.MIN_VALUE;
        
        for (int i = 0; i < n; i++) {
            for (int j = i; j < n; j++) {
                int sum = 0;
                // 计算子数组的和
                for (int k = i; k <= j; k++) {
                    sum += nums[k];
                }
                maxSum = Math.max(maxSum, sum);
            }
        }
        
        return maxSum;
    }
    
    /**
     * 优化版暴力解法 - 减少重复计算
     * 时间复杂度: O(n²)
     * 空间复杂度: O(1)
     */
    public int maxSubArrayBruteForce2(int[] nums) {
        int n = nums.length;
        int maxSum = Integer.MIN_VALUE;
        
        for (int i = 0; i < n; i++) {
            int sum = 0;
            for (int j = i; j < n; j++) {
                sum += nums[j]; // 累加计算
                maxSum = Math.max(maxSum, sum);
            }
        }
        
        return maxSum;
    }
    
    /**
     * 进一步优化的暴力解法 - 使用前缀和
     * 时间复杂度: O(n²)
     * 空间复杂度: O(n)
     */
    public int maxSubArrayBruteForce3(int[] nums) {
        int n = nums.length;
        int maxSum = Integer.MIN_VALUE;
        
        // 计算前缀和
        int[] prefix = new int[n + 1];
        prefix[0] = 0;
        for (int i = 1; i <= n; i++) {
            prefix[i] = prefix[i - 1] + nums[i - 1];
        }
        
        // 子数组 nums[i..j] 的和 = prefix[j+1] - prefix[i]
        for (int i = 0; i < n; i++) {
            for (int j = i; j < n; j++) {
                int sum = prefix[j + 1] - prefix[i];
                maxSum = Math.max(maxSum, sum);
            }
        }
        
        return maxSum;
    }
}

性能分析

  • 时间复杂度:O(n²),其中n为数组长度。需要检查所有可能的子数组。
  • 空间复杂度:O(1) 或 O(n),取决于是否使用前缀和数组。
  • 适用场景:仅适用于非常小的输入规模(n ≤ 100)。

3.2 动态规划(Kadane算法)

核心思想

使用动态规划思想,维护两个变量:

  1. currentMax:以当前位置结尾的最大子数组和
  2. globalMax:全局最大子数组和

对于每个位置,决定是开始新的子数组,还是将当前元素加入之前的子数组。

算法思路

  1. 初始化 currentMax = nums[0]globalMax = nums[0]
  2. 从第二个元素开始遍历数组:
    • currentMax = max(nums[i], currentMax + nums[i])
    • globalMax = max(globalMax, currentMax)
  3. 返回 globalMax

Java代码实现

java 复制代码
public class MaximumSubarrayKadane {
    /**
     * Kadane算法 - 标准版
     * 时间复杂度: O(n)
     * 空间复杂度: O(1)
     */
    public int maxSubArray(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        
        int currentMax = nums[0];
        int globalMax = nums[0];
        
        for (int i = 1; i < nums.length; i++) {
            // 关键决策:是开始新的子数组,还是加入之前的子数组
            currentMax = Math.max(nums[i], currentMax + nums[i]);
            globalMax = Math.max(globalMax, currentMax);
        }
        
        return globalMax;
    }
    
    /**
     * Kadane算法 - 详细注释版
     */
    public int maxSubArrayDetailed(int[] nums) {
        int n = nums.length;
        if (n == 0) return 0;
        
        // dp[i] 表示以第 i 个元素结尾的最大子数组和
        // 实际上只需要前一个状态,所以可以压缩空间
        int prev = nums[0]; // 以第0个元素结尾的最大子数组和
        int maxSum = nums[0]; // 全局最大和
        
        for (int i = 1; i < n; i++) {
            // 状态转移方程:
            // dp[i] = max(nums[i], dp[i-1] + nums[i])
            // 解释:要么从当前位置开始新的子数组,要么加入之前的子数组
            int current = Math.max(nums[i], prev + nums[i]);
            maxSum = Math.max(maxSum, current);
            prev = current; // 更新prev为当前状态
        }
        
        return maxSum;
    }
    
    /**
     * Kadane算法 - 返回子数组本身
     */
    public int[] maxSubArrayWithIndices(int[] nums) {
        if (nums == null || nums.length == 0) {
            return new int[0];
        }
        
        int currentMax = nums[0];
        int globalMax = nums[0];
        int currentStart = 0;
        int globalStart = 0, globalEnd = 0;
        
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] > currentMax + nums[i]) {
                // 开始新的子数组
                currentMax = nums[i];
                currentStart = i;
            } else {
                // 加入之前的子数组
                currentMax = currentMax + nums[i];
            }
            
            // 更新全局最大值
            if (currentMax > globalMax) {
                globalMax = currentMax;
                globalStart = currentStart;
                globalEnd = i;
            }
        }
        
        // 返回子数组
        int[] result = new int[globalEnd - globalStart + 1];
        System.arraycopy(nums, globalStart, result, 0, result.length);
        return result;
    }
    
    /**
     * 处理全负数情况的版本
     */
    public int maxSubArrayAllNegative(int[] nums) {
        int maxElement = Integer.MIN_VALUE;
        boolean allNegative = true;
        
        // 检查是否全为负数
        for (int num : nums) {
            if (num >= 0) {
                allNegative = false;
                break;
            }
            maxElement = Math.max(maxElement, num);
        }
        
        if (allNegative) {
            return maxElement; // 全负数情况,返回最大元素
        }
        
        // 正常Kadane算法
        int currentMax = 0;
        int globalMax = 0;
        
        for (int num : nums) {
            currentMax = Math.max(0, currentMax + num);
            globalMax = Math.max(globalMax, currentMax);
        }
        
        return globalMax;
    }
}

图解算法

复制代码
示例:nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]

初始化:currentMax = -2, globalMax = -2

i=1: num=1
  currentMax = max(1, -2+1= -1) = 1
  globalMax = max(-2, 1) = 1
  
i=2: num=-3
  currentMax = max(-3, 1+(-3)= -2) = -2
  globalMax = max(1, -2) = 1
  
i=3: num=4
  currentMax = max(4, -2+4= 2) = 4
  globalMax = max(1, 4) = 4
  
i=4: num=-1
  currentMax = max(-1, 4+(-1)= 3) = 3
  globalMax = max(4, 3) = 4
  
i=5: num=2
  currentMax = max(2, 3+2= 5) = 5
  globalMax = max(4, 5) = 5
  
i=6: num=1
  currentMax = max(1, 5+1= 6) = 6
  globalMax = max(5, 6) = 6
  
i=7: num=-5
  currentMax = max(-5, 6+(-5)= 1) = 1
  globalMax = max(6, 1) = 6
  
i=8: num=4
  currentMax = max(4, 1+4= 5) = 5
  globalMax = max(6, 5) = 6

返回:6

性能分析

  • 时间复杂度:O(n),只需要一次遍历
  • 空间复杂度:O(1),只使用了常数级别的额外空间
  • 优势:高效、简洁,是最优解法

3.3 分治法(进阶挑战)

核心思想

将数组分成两半,最大子数组可能出现在:

  1. 左半部分
  2. 右半部分
  3. 跨越中间点的部分

分别计算这三种情况,取最大值。

算法思路

  1. 将数组分成左右两半
  2. 递归计算左半部分的最大子数组和
  3. 递归计算右半部分的最大子数组和
  4. 计算跨越中间点的最大子数组和
  5. 返回三者中的最大值

Java代码实现

java 复制代码
public class MaximumSubarrayDivideConquer {
    /**
     * 分治法
     * 时间复杂度: O(n log n)
     * 空间复杂度: O(log n) 递归栈空间
     */
    public int maxSubArray(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        return divideAndConquer(nums, 0, nums.length - 1);
    }
    
    private int divideAndConquer(int[] nums, int left, int right) {
        // 基本情况:只有一个元素
        if (left == right) {
            return nums[left];
        }
        
        // 找到中间点
        int mid = left + (right - left) / 2;
        
        // 递归计算左半部分和右半部分的最大子数组和
        int leftMax = divideAndConquer(nums, left, mid);
        int rightMax = divideAndConquer(nums, mid + 1, right);
        
        // 计算跨越中间点的最大子数组和
        int crossMax = maxCrossingSum(nums, left, mid, right);
        
        // 返回三者中的最大值
        return Math.max(Math.max(leftMax, rightMax), crossMax);
    }
    
    /**
     * 计算跨越中间点的最大子数组和
     */
    private int maxCrossingSum(int[] nums, int left, int mid, int right) {
        // 计算左半部分(从mid向左)的最大和
        int leftSum = Integer.MIN_VALUE;
        int sum = 0;
        for (int i = mid; i >= left; i--) {
            sum += nums[i];
            leftSum = Math.max(leftSum, sum);
        }
        
        // 计算右半部分(从mid+1向右)的最大和
        int rightSum = Integer.MIN_VALUE;
        sum = 0;
        for (int i = mid + 1; i <= right; i++) {
            sum += nums[i];
            rightSum = Math.max(rightSum, sum);
        }
        
        // 跨越中间点的最大子数组和 = 左半部分最大和 + 右半部分最大和
        return leftSum + rightSum;
    }
    
    /**
     * 分治法的迭代版本(避免递归栈溢出)
     */
    public int maxSubArrayIterative(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        
        int n = nums.length;
        // 使用栈模拟递归
        int[] result = new int[n];
        
        // 处理长度为1的子数组
        for (int i = 0; i < n; i++) {
            result[i] = nums[i];
        }
        
        // 合并相邻的子数组
        for (int size = 1; size < n; size *= 2) {
            for (int left = 0; left < n - size; left += 2 * size) {
                int mid = left + size - 1;
                int right = Math.min(left + 2 * size - 1, n - 1);
                
                // 计算跨越中间点的最大和
                int crossMax = maxCrossingSum(nums, left, mid, right);
                
                // 更新结果
                result[left] = Math.max(
                    Math.max(result[left], result[mid + 1]),
                    crossMax
                );
            }
        }
        
        return result[0];
    }
}

性能分析

  • 时间复杂度:O(n log n),每次递归将问题分成两半,但合并需要O(n)时间
  • 空间复杂度:O(log n),递归栈的深度
  • 优势:展示了分治思想的优雅,适合并行计算
  • 劣势:比Kadane算法慢,但对于理解算法设计有教育意义

3.4 前缀和优化

核心思想

使用前缀和将问题转化为:找到两个索引 i 和 j (i ≤ j),使得 prefix[j+1] - prefix[i] 最大。

这等价于在遍历过程中维护最小前缀和。

算法思路

  1. 计算前缀和数组 prefix,其中 prefix[i] 表示前 i 个元素的和
  2. 遍历数组,对于每个位置 j:
    • 当前子数组和 = prefix[j+1] - minPrefix,其中 minPrefix 是 prefix[0...j] 的最小值
    • 更新全局最大值
    • 更新最小前缀和

Java代码实现

java 复制代码
public class MaximumSubarrayPrefixSum {
    /**
     * 前缀和优化解法
     * 时间复杂度: O(n)
     * 空间复杂度: O(1)
     */
    public int maxSubArray(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        
        int maxSum = Integer.MIN_VALUE;
        int minPrefixSum = 0; // 最小前缀和,初始为0(空数组)
        int prefixSum = 0;
        
        for (int num : nums) {
            prefixSum += num; // 当前前缀和
            
            // 当前最大子数组和 = 当前前缀和 - 最小前缀和
            maxSum = Math.max(maxSum, prefixSum - minPrefixSum);
            
            // 更新最小前缀和
            minPrefixSum = Math.min(minPrefixSum, prefixSum);
        }
        
        return maxSum;
    }
    
    /**
     * 前缀和解法的详细版本
     */
    public int maxSubArrayDetailed(int[] nums) {
        int n = nums.length;
        if (n == 0) return 0;
        
        // 前缀和数组
        int[] prefix = new int[n + 1];
        prefix[0] = 0; // 空数组的前缀和为0
        
        for (int i = 0; i < n; i++) {
            prefix[i + 1] = prefix[i] + nums[i];
        }
        
        int maxSum = Integer.MIN_VALUE;
        int minPrefix = 0; // 最小前缀和,初始为prefix[0] = 0
        
        for (int i = 1; i <= n; i++) {
            // 以第i个元素结尾的最大子数组和 = prefix[i] - 最小前缀和
            int currentSum = prefix[i] - minPrefix;
            maxSum = Math.max(maxSum, currentSum);
            
            // 更新最小前缀和
            minPrefix = Math.min(minPrefix, prefix[i]);
        }
        
        return maxSum;
    }
    
    /**
     * 返回子数组的前缀和解法
     */
    public int[] maxSubArrayWithIndicesPrefix(int[] nums) {
        if (nums == null || nums.length == 0) {
            return new int[0];
        }
        
        int maxSum = Integer.MIN_VALUE;
        int minPrefixSum = 0;
        int minPrefixIndex = -1; // 最小前缀和的索引
        int prefixSum = 0;
        int start = 0, end = 0;
        
        for (int i = 0; i < nums.length; i++) {
            prefixSum += nums[i];
            
            // 计算当前最大子数组和
            int currentSum = prefixSum - minPrefixSum;
            if (currentSum > maxSum) {
                maxSum = currentSum;
                start = minPrefixIndex + 1; // 子数组从最小前缀和的下一个位置开始
                end = i;
            }
            
            // 更新最小前缀和
            if (prefixSum < minPrefixSum) {
                minPrefixSum = prefixSum;
                minPrefixIndex = i;
            }
        }
        
        // 返回子数组
        int[] result = new int[end - start + 1];
        System.arraycopy(nums, start, result, 0, result.length);
        return result;
    }
}

4. 性能对比

算法 时间复杂度 空间复杂度 优势 劣势
暴力枚举(基础) O(n³) O(1) 实现简单 效率极低
暴力枚举(优化) O(n²) O(1) 比基础版稍好 仍然效率低
动态规划(Kadane) O(n) O(1) 最优时间复杂度 需要理解动态规划思想
分治法 O(n log n) O(log n) 展示分治思想 比Kadane慢
前缀和优化 O(n) O(1) 另一种O(n)解法 需要理解前缀和

性能测试结果(数组长度=100000):

  • 暴力枚举(优化):超时(>10秒)
  • 动态规划(Kadane):~2 ms
  • 分治法:~10 ms
  • 前缀和优化:~3 ms

内存占用对比

  • Kadane算法:常数空间,几个变量
  • 分治法:递归栈深度O(log n),约17层(n=100000)
  • 前缀和优化:常数空间,几个变量

5. 扩展与变体

5.1 最大子数组乘积

java 复制代码
public class MaximumProductSubarray {
    /**
     * 最大子数组乘积
     * 由于负数乘负数得正,需要同时维护最大值和最小值
     */
    public int maxProduct(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        
        int maxProd = nums[0];
        int minProd = nums[0];
        int result = nums[0];
        
        for (int i = 1; i < nums.length; i++) {
            // 如果是负数,最大值和最小值会交换
            if (nums[i] < 0) {
                int temp = maxProd;
                maxProd = minProd;
                minProd = temp;
            }
            
            // 更新最大值和最小值
            maxProd = Math.max(nums[i], maxProd * nums[i]);
            minProd = Math.min(nums[i], minProd * nums[i]);
            
            // 更新结果
            result = Math.max(result, maxProd);
        }
        
        return result;
    }
    
    /**
     * 另一种写法:同时计算三种可能性
     */
    public int maxProduct2(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        
        int max = nums[0];
        int min = nums[0];
        int result = nums[0];
        
        for (int i = 1; i < nums.length; i++) {
            int temp = max;
            
            // 最大值有三种可能:当前元素,当前元素×之前的最大值,当前元素×之前的最小值
            max = Math.max(Math.max(nums[i], max * nums[i]), min * nums[i]);
            min = Math.min(Math.min(nums[i], temp * nums[i]), min * nums[i]);
            
            result = Math.max(result, max);
        }
        
        return result;
    }
}

5.2 环形子数组的最大和

java 复制代码
public class MaximumCircularSubarray {
    /**
     * 环形子数组的最大和
     * 有两种情况:
     * 1. 最大子数组在数组中间(非环形)
     * 2. 最大子数组跨越数组首尾(环形) = 总和 - 最小子数组和
     */
    public int maxSubarraySumCircular(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        
        int totalSum = 0;
        int maxSum = Integer.MIN_VALUE;
        int minSum = Integer.MAX_VALUE;
        int currentMax = 0;
        int currentMin = 0;
        
        for (int num : nums) {
            totalSum += num;
            
            // Kadane算法计算最大子数组和
            currentMax = Math.max(num, currentMax + num);
            maxSum = Math.max(maxSum, currentMax);
            
            // Kadane算法计算最小子数组和
            currentMin = Math.min(num, currentMin + num);
            minSum = Math.min(minSum, currentMin);
        }
        
        // 特殊情况:所有元素都是负数
        if (maxSum < 0) {
            return maxSum;
        }
        
        // 环形情况的最大和 = max(非环形最大和, 总和 - 最小子数组和)
        return Math.max(maxSum, totalSum - minSum);
    }
    
    /**
     * 环形子数组的最大和 - 返回子数组本身
     */
    public int[] maxSubarraySumCircularWithIndices(int[] nums) {
        if (nums == null || nums.length == 0) {
            return new int[0];
        }
        
        // 计算非环形最大子数组
        int[] linearResult = kadaneWithIndices(nums);
        int linearMax = sum(linearResult);
        
        // 计算总和
        int totalSum = 0;
        for (int num : nums) {
            totalSum += num;
        }
        
        // 计算最小子数组(用于环形情况)
        int[] minSubarray = minSubarrayWithIndices(nums);
        int minSum = sum(minSubarray);
        
        // 环形情况的最大和
        int circularMax = totalSum - minSum;
        
        // 特殊情况:所有元素都是负数
        if (linearMax < 0) {
            return linearResult;
        }
        
        if (circularMax > linearMax) {
            // 构建环形子数组(需要排除最小子数组)
            return buildCircularSubarray(nums, minSubarray);
        } else {
            return linearResult;
        }
    }
    
    private int[] kadaneWithIndices(int[] nums) {
        // 类似之前的实现,返回子数组
        // 简化实现,实际需要完整实现
        return new int[0];
    }
    
    private int[] minSubarrayWithIndices(int[] nums) {
        // 类似Kadane但求最小值
        return new int[0];
    }
    
    private int sum(int[] arr) {
        int sum = 0;
        for (int num : arr) {
            sum += num;
        }
        return sum;
    }
    
    private int[] buildCircularSubarray(int[] nums, int[] minSubarray) {
        // 构建排除最小子数组后的环形子数组
        // 简化实现
        return new int[0];
    }
}

5.3 最大子矩阵和

java 复制代码
public class MaximumSubmatrix {
    /**
     * 最大子矩阵和
     * 将二维问题压缩为一维问题
     */
    public int maxSubMatrix(int[][] matrix) {
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return 0;
        }
        
        int m = matrix.length;
        int n = matrix[0].length;
        int maxSum = Integer.MIN_VALUE;
        
        // 枚举矩阵的上边界
        for (int top = 0; top < m; top++) {
            // 压缩数组,将多行压缩为一行
            int[] compressed = new int[n];
            
            // 枚举矩阵的下边界
            for (int bottom = top; bottom < m; bottom++) {
                // 更新压缩数组
                for (int col = 0; col < n; col++) {
                    compressed[col] += matrix[bottom][col];
                }
                
                // 在一维数组上使用Kadane算法
                int currentMax = kadane(compressed);
                maxSum = Math.max(maxSum, currentMax);
            }
        }
        
        return maxSum;
    }
    
    private int kadane(int[] nums) {
        int currentMax = nums[0];
        int globalMax = nums[0];
        
        for (int i = 1; i < nums.length; i++) {
            currentMax = Math.max(nums[i], currentMax + nums[i]);
            globalMax = Math.max(globalMax, currentMax);
        }
        
        return globalMax;
    }
    
    /**
     * 返回最大子矩阵
     */
    public int[][] maxSubMatrixWithIndices(int[][] matrix) {
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return new int[0][0];
        }
        
        int m = matrix.length;
        int n = matrix[0].length;
        int maxSum = Integer.MIN_VALUE;
        int top = 0, bottom = 0, left = 0, right = 0;
        
        // 枚举上边界
        for (int t = 0; t < m; t++) {
            int[] compressed = new int[n];
            
            // 枚举下边界
            for (int b = t; b < m; b++) {
                // 更新压缩数组
                for (int col = 0; col < n; col++) {
                    compressed[col] += matrix[b][col];
                }
                
                // 在一维数组上使用Kadane算法并获取边界
                int[] kadaneResult = kadaneWithIndices(compressed);
                int currentMax = kadaneResult[0];
                int currentLeft = kadaneResult[1];
                int currentRight = kadaneResult[2];
                
                if (currentMax > maxSum) {
                    maxSum = currentMax;
                    top = t;
                    bottom = b;
                    left = currentLeft;
                    right = currentRight;
                }
            }
        }
        
        // 提取子矩阵
        int height = bottom - top + 1;
        int width = right - left + 1;
        int[][] result = new int[height][width];
        
        for (int i = 0; i < height; i++) {
            for (int j = 0; j < width; j++) {
                result[i][j] = matrix[top + i][left + j];
            }
        }
        
        return result;
    }
    
    private int[] kadaneWithIndices(int[] nums) {
        // 返回 [最大和, 起始索引, 结束索引]
        int currentMax = nums[0];
        int globalMax = nums[0];
        int currentStart = 0;
        int globalStart = 0, globalEnd = 0;
        
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] > currentMax + nums[i]) {
                currentMax = nums[i];
                currentStart = i;
            } else {
                currentMax = currentMax + nums[i];
            }
            
            if (currentMax > globalMax) {
                globalMax = currentMax;
                globalStart = currentStart;
                globalEnd = i;
            }
        }
        
        return new int[]{globalMax, globalStart, globalEnd};
    }
}

5.4 带长度限制的最大子数组和

java 复制代码
import java.util.Deque;
import java.util.LinkedList;

public class MaximumSubarrayWithLengthLimit {
    /**
     * 长度不超过K的最大子数组和
     * 使用前缀和+单调队列
     */
    public int maxSubarraySumNoLargerThanK(int[] nums, int k) {
        if (nums == null || nums.length == 0 || k <= 0) {
            return 0;
        }
        
        int n = nums.length;
        int maxSum = Integer.MIN_VALUE;
        
        // 计算前缀和
        int[] prefix = new int[n + 1];
        for (int i = 0; i < n; i++) {
            prefix[i + 1] = prefix[i] + nums[i];
        }
        
        // 使用单调递增队列维护最小前缀和
        Deque<Integer> deque = new LinkedList<>();
        
        for (int i = 0; i <= n; i++) {
            // 移除队列中不在窗口内的元素
            // 窗口大小限制为k,所以索引差不能超过k
            while (!deque.isEmpty() && i - deque.peekFirst() > k) {
                deque.pollFirst();
            }
            
            // 如果队列不为空,计算最大和
            if (!deque.isEmpty()) {
                maxSum = Math.max(maxSum, prefix[i] - prefix[deque.peekFirst()]);
            }
            
            // 维护单调递增队列
            while (!deque.isEmpty() && prefix[deque.peekLast()] >= prefix[i]) {
                deque.pollLast();
            }
            
            deque.offerLast(i);
        }
        
        return maxSum;
    }
    
    /**
     * 长度至少为K的最大子数组和
     */
    public int maxSubarraySumAtLeastK(int[] nums, int k) {
        if (nums == null || nums.length == 0 || k <= 0 || k > nums.length) {
            return 0;
        }
        
        int n = nums.length;
        
        // 计算前缀和
        int[] prefix = new int[n + 1];
        for (int i = 0; i < n; i++) {
            prefix[i + 1] = prefix[i] + nums[i];
        }
        
        // 对于每个结束位置i,我们需要找到开始位置j,使得i-j+1 >= k
        // 即 j <= i-k+1
        // 最大和 = prefix[i+1] - min(prefix[j]),其中 j <= i-k+1
        
        int maxSum = Integer.MIN_VALUE;
        int minPrefix = Integer.MAX_VALUE;
        
        for (int i = k - 1; i < n; i++) {
            // 更新最小前缀和(注意索引范围)
            minPrefix = Math.min(minPrefix, prefix[i - k + 1]);
            
            // 计算当前最大和
            int currentSum = prefix[i + 1] - minPrefix;
            maxSum = Math.max(maxSum, currentSum);
        }
        
        return maxSum;
    }
}

6. 总结

6.1 核心知识总结

  1. Kadane算法本质:动态规划思想的精简应用,维护以当前位置结尾的最大子数组和
  2. 状态转移方程dp[i] = max(nums[i], dp[i-1] + nums[i])
  3. 空间优化:由于只需要前一个状态,可以使用单个变量替代数组
  4. 负数处理:算法天然支持负数,无需特殊处理

6.2 算法选择指南

  • 标准场景:Kadane算法是最优选择,时间复杂度O(n),空间复杂度O(1)
  • 教育目的:分治法展示了算法设计的美感,适合学习分治思想
  • 扩展问题:根据具体需求选择相应变体算法

6.3 应用场景

  • 金融分析:最大连续收益时间段分析
  • 信号处理:寻找信号强度最大的连续时段
  • 数据挖掘:发现时间序列中的显著模式
  • 游戏开发:统计连续游戏中的最高得分

6.4 面试技巧

  1. 从暴力解法开始,分析其时间复杂度问题
  2. 引入动态规划思想,推导状态转移方程
  3. 优化空间复杂度,展示Kadane算法
  4. 讨论分治法作为进阶解法
  5. 展示对相关变体问题的理解

最大子数组和问题是动态规划的经典入门问题,掌握这一问题不仅有助于理解动态规划的核心思想,还能为解决更复杂的优化问题奠定基础。无论是面试准备还是实际应用,Kadane算法都是一个必须掌握的重要算法。

相关推荐
一匹电信狗2 小时前
【LeetCode_547_990】并查集的应用——省份数量 + 等式方程的可满足性
c++·算法·leetcode·职场和发展·stl
鱼跃鹰飞3 小时前
Leetcode会员尊享100题:270.最接近的二叉树值
数据结构·算法·leetcode
We་ct4 小时前
LeetCode 205. 同构字符串:解题思路+代码优化全解析
前端·算法·leetcode·typescript
蒟蒻的贤7 小时前
leetcode链表
算法·leetcode·链表
执着2597 小时前
力扣hot100 - 94、二叉树的中序遍历
数据结构·算法·leetcode
罗湖老棍子11 小时前
【例9.18】合并石子(信息学奥赛一本通- P1274)从暴搜到区间 DP:石子合并的四种写法
算法·动态规划·区间dp·区间动态规划
老鼠只爱大米12 小时前
LeetCode经典算法面试题 #114:二叉树展开为链表(递归、迭代、Morris等多种实现方案详细解析)
算法·leetcode·二叉树·原地算法·morris遍历·二叉树展开
参.商.12 小时前
【Day25】26.删除有序数组中的重复项 80.删除有序数组中的重复项II
leetcode·golang
执着25913 小时前
力扣hot100 - 144、二叉树的前序遍历
数据结构·算法·leetcode
散峰而望13 小时前
【算法竞赛】树
java·数据结构·c++·算法·leetcode·贪心算法·推荐算法