当前位置: 首页 > news >正文

最大子数组和问题-详解Kadane算法

最大子数组和问题-详解Kadane算法

    • 一、问题定义与暴力解法
      • 1.1 问题描述
      • 1.2 暴力解法的低效性
    • 二、Kadane算法的核心原理
      • 2.1 动态规划思想的应用
      • 2.2 优化空间复杂度
    • 三、Kadane算法的Java实现
      • 3.1 基础版本(处理所有情况)
      • 3.2 算法正确性验证
    • 四、Kadane算法的变种与拓展
      • 4.1 变种1:输出最大子数组的起止索引
      • 4.2 变种2:限制子数组长度(最多`k`个元素)
      • 4.3 变种3:允许子数组为空(返回0)
    • 五、时间与空间复杂度
    • 六、实际应用场景

最大子数组和(Maximum Subarray Sum)是一个经典且高频的数组算法考点,这个问题看似简单——从一个整数数组中找到和最大的连续子数组,但暴力求解的效率极低。Kadane算法(卡丹算法)作为专门解决此问题的高效方法,能在O(n)O(n)O(n)时间内完成求解,是动态规划思想的典型应用。本文我将深入解析Kadane算法的核心原理、实现细节、变种拓展及实际应用,结合Java代码示例,帮你彻底掌握这一高效算法。

一、问题定义与暴力解法

1.1 问题描述

给定一个整数数组nums(可能包含负数),找到一个连续子数组(至少包含一个元素),使得该子数组的和最大,返回这个最大和。

例如:

  • 输入:nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
  • 输出:6(对应子数组[4, -1, 2, 1]

1.2 暴力解法的低效性

最直观的思路是枚举所有可能的子数组,计算其和并记录最大值:

// 暴力解法:枚举所有子数组
public int maxSubArrayBruteForce(int[] nums) {int maxSum = Integer.MIN_VALUE;int n = nums.length;for (int i = 0; i < n; i++) {  // 子数组起点int currentSum = 0;for (int j = i; j < n; j++) {  // 子数组终点currentSum += nums[j];maxSum = Math.max(maxSum, currentSum);}}return maxSum;
}
  • 时间复杂度O(n2)O(n^2)O(n2)(嵌套循环枚举所有子数组)。
  • 局限性:当n超过10410^4104时,会因超时无法通过测试,必须寻找更高效的算法。

二、Kadane算法的核心原理

2.1 动态规划思想的应用

Kadane算法的本质是动态规划,其核心是用局部最优推导全局最优

  • 定义dp[i]为“以nums[i]为结尾的最大子数组和”。
  • 对于每个元素nums[i],有两种选择:
    1. nums[i]加入之前的子数组(即dp[i-1] + nums[i])。
    2. nums[i]为起点重新开始一个子数组(即nums[i])。
  • 因此,状态转移方程为:
    dp[i] = max(nums[i], dp[i-1] + nums[i])
  • 全局最大和即为所有dp[i]中的最大值。

2.2 优化空间复杂度

由于dp[i]仅依赖于dp[i-1],无需存储整个dp数组,可改用一个变量currentMax记录当前值,将空间复杂度从O(n)O(n)O(n)优化至O(1)O(1)O(1)

  1. 初始化currentMax = nums[0](以第一个元素为结尾的子数组和),maxSum = nums[0](全局最大值)。
  2. 从第二个元素开始遍历:
    • currentMax = max(nums[i], currentMax + nums[i])(更新局部最优)。
    • maxSum = max(maxSum, currentMax)(更新全局最优)。
  3. 遍历结束后,maxSum即为结果。

三、Kadane算法的Java实现

3.1 基础版本(处理所有情况)

public class KadaneAlgorithm {public int maxSubArray(int[] nums) {if (nums == null || nums.length == 0) {return 0;  // 边界处理:空数组}int currentMax = nums[0];  // 以当前元素为结尾的最大子数组和int maxSum = nums[0];      // 全局最大子数组和for (int i = 1; i < nums.length; i++) {// 选择:加入之前的子数组 或 重新开始currentMax = Math.max(nums[i], currentMax + nums[i]);// 更新全局最大值maxSum = Math.max(maxSum, currentMax);}return maxSum;}public static void main(String[] args) {KadaneAlgorithm solution = new KadaneAlgorithm();int[] nums = {-2, 1, -3, 4, -1, 2, 1, -5, 4};System.out.println(solution.maxSubArray(nums));  // 输出 6}
}

3.2 算法正确性验证

以示例nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]为例,分步验证:

索引inums[i]currentMax(局部最优)maxSum(全局最优)
0-2-2-2
11max(1, -2+1)=1max(-2, 1)=1
2-3max(-3, 1-3)=-2max(1, -2)=1
34max(4, -2+4)=4max(1, 4)=4
4-1max(-1, 4-1)=3max(4, 3)=4
52max(2, 3+2)=5max(4, 5)=5
61max(1, 5+1)=6max(5, 6)=6
7-5max(-5, 6-5)=1max(6, 1)=6
84max(4, 1+4)=5max(6, 5)=6

最终maxSum=6,与预期结果一致,验证了算法的正确性。

四、Kadane算法的变种与拓展

4.1 变种1:输出最大子数组的起止索引

除了最大和,有时需要返回子数组的具体位置(起点和终点索引)。只需在更新currentMaxmaxSum时,同步记录索引:

public int[] maxSubArrayWithIndex(int[] nums) {if (nums == null || nums.length == 0) {return new int[]{-1, -1};  // 空数组返回无效索引}int currentMax = nums[0];int maxSum = nums[0];int start = 0, end = 0;  // 当前子数组起止索引int tempStart = 0;       // 临时起点(用于重新开始子数组时更新)for (int i = 1; i < nums.length; i++) {if (nums[i] > currentMax + nums[i]) {// 重新开始子数组,更新临时起点currentMax = nums[i];tempStart = i;} else {// 加入之前的子数组currentMax += nums[i];}// 更新全局最大和及起止索引if (currentMax > maxSum) {maxSum = currentMax;start = tempStart;end = i;}}return new int[]{start, end, maxSum};  // 起点、终点、最大和
}

4.2 变种2:限制子数组长度(最多k个元素)

问题:找到长度不超过k的连续子数组的最大和。
思路:结合Kadane算法和前缀和优化,用滑动窗口维护前缀和的最小值(需额外处理长度限制):

public int maxSubArrayWithLimit(int[] nums, int k) {int n = nums.length;int[] prefixSum = new int[n + 1];  // 前缀和:prefixSum[i] = sum(nums[0..i-1])for (int i = 0; i < n; i++) {prefixSum[i + 1] = prefixSum[i] + nums[i];}int maxSum = Integer.MIN_VALUE;// 用单调队列维护前缀和的最小值(窗口内)Deque<Integer> deque = new ArrayDeque<>();deque.offer(0);  // 初始前缀和prefixSum[0] = 0for (int i = 1; i <= n; i++) {// 移除窗口外的前缀和(超过k个元素)while (!deque.isEmpty() && i - deque.peekFirst() > k) {deque.pollFirst();}// 当前前缀和 - 窗口内最小前缀和 = 最大子数组和maxSum = Math.max(maxSum, prefixSum[i] - prefixSum[deque.peekFirst()]);// 维护单调队列(保证前缀和递增)while (!deque.isEmpty() && prefixSum[i] <= prefixSum[deque.peekLast()]) {deque.pollLast();}deque.offer(i);}return maxSum;
}

4.3 变种3:允许子数组为空(返回0)

若题目允许子数组为空(即最大和可以是0,如所有元素为负数时),只需在最后对结果取max(0, maxSum)

public int maxSubArrayAllowEmpty(int[] nums) {int currentMax = 0;  // 初始化为0(允许空数组)int maxSum = 0;for (int num : nums) {currentMax = Math.max(0, currentMax + num);  // 空数组对应0maxSum = Math.max(maxSum, currentMax);}return maxSum;
}

五、时间与空间复杂度

  • 时间复杂度O(n)O(n)O(n),仅需遍历数组一次,每次操作均为常数时间。
  • 空间复杂度O(1)O(1)O(1),仅使用固定数量的变量(基础版本),适合大规模数据。

六、实际应用场景

  1. 股票收益分析:计算某段时间内连续持有股票的最大收益(将股价数组转为每日涨跌数组)。
  2. 信号处理:在噪声信号中提取连续有效信号段(信号强度和最大的区间)。
  3. 能源消耗优化:找到连续时间段内能源消耗最高的区间,用于负载均衡。
  4. LeetCode经典题:LeetCode 53(最大子数组和)、LeetCode 152(乘积最大子数组,Kadane算法的变种)。

总结
Kadane算法通过动态规划思想,以O(n)O(n)O(n)时间和O(1)O(1)O(1)空间高效解决最大子数组和问题,是算法优化的典型案例。其核心在于“局部最优选择”——每个元素要么加入之前的子数组,要么重新开始,通过不断更新局部最优和全局最优得到结果。
在实际应用中需注意:

  • 基础版本可直接解决无长度限制的最大子数组和问题。
  • 变种问题(如限制长度、返回索引)可通过扩展算法逻辑实现。
  • 结合前缀和、单调队列等工具,可处理更复杂的场景。

That’s all, thanks for reading~~
觉得有用就点个赞、收进收藏夹吧!关注我,获取更多干货~

http://www.dtcms.com/a/288652.html

相关文章:

  • 每日一题7.20
  • OSS文件上传(一):简单上传
  • feignClient 调用详细流程
  • Valgrind Memcheck 全解析教程:6个程序说明基础内存错误
  • 判断一个数是否为质数方法
  • VSCode使用Jupyter完整指南配置机器学习环境
  • c#:TCP服务端管理类
  • 正点原子stm32F407学习笔记10——输入捕获实验
  • 2025 年科技革命时刻表:四大关键节点将如何重塑未来?
  • 内网后渗透攻击过程(实验环境)--3、横向攻击
  • SQL 调优第一步:EXPLAIN 关键字全解析
  • 【已解决】GitHub SSH 连接失败解决方案:Permission Denied (publickey) 错误修复指南
  • [Linux]进程 / PID
  • 30天打牢数模基础-决策树讲解
  • Linux入门篇学习——NFS 服务器的搭建和使用和开发板固件烧录
  • Spring Boot 第一天知识汇总
  • 【Java项目安全基石】登录认证实战:Session/Token/JWT用户校验机制深度解析
  • 相似度计算
  • 「Java案例」利用方法求反素数
  • Facebook 开源多季节性时间序列数据预测工具:Prophet 饱和预测 Saturating Forecasts
  • dynamic_cast的实现原理
  • Beamer-LaTeX学习(教程批注版)【6】
  • Elasticsearch 简化指南:GCP Google Compute Engine
  • GPT-4o mini TTS:领先的文本转语音技术
  • 随着GPT-5测试中泄露OpenAI 预计将很快发布 揭秘GPT-5冲击波:OpenAI如何颠覆AI战场,碾压谷歌和Claude?
  • prometheus 黑盒监控和docker检测
  • mysql第三次作业
  • 学习寄存器——GPIO(二)学习BSRR BRR ODR寄存器的原子性和在HAL库的应用
  • 【Go语言-Day 22】解耦与多态的基石:深入理解 Go 接口 (Interface) 的核心概念
  • 【详细笔记】两类曲线积分转换