LeetCode 918. 环形子数组的最大和:两种解法详解

刷题路上遇到环形数组的问题,总容易被"环形"这个条件绕晕------子数组不仅能是常规的连续片段,还能跨数组首尾连接。今天就来拆解 LeetCode 918 题「环形子数组的最大和」,分享两种高效解法,从原理到代码一步步讲透,帮你彻底搞懂这类环形数组问题。

先看题目核心:给定一个长度为 n 的环形整数数组 nums,返回非空子数组的最大可能和。这里要注意两个关键约束:一是环形意味着数组首尾相连,二是子数组不能重复使用元素(也就是说,跨首尾的子数组比如 nums[n-1], nums[0], nums[1] 是允许的,但不能包含 nums[0] 两次)。

题目核心难点

常规的子数组最大和(比如 LeetCode 53 题),用 Kadane 算法就能轻松解决,但环形数组多了"跨首尾"的情况,这就需要我们跳出常规思维:

  • 常规子数组:从 i 到 j(i ≤ j),连续且不跨首尾;

  • 环形子数组:从 j 到 n-1,再从 0 到 i(j > i),本质是"数组总和 - 中间一段最小子数组的和"。

基于这个思路,衍生出两种经典解法,下面分别详细讲解。

解法一:全局最大值 = max(常规最大和, 总和 - 常规最小和)

核心原理

这是最直观、最易理解的解法,核心逻辑分两种情况:

  1. 最大子数组不跨首尾:就是常规的子数组最大和,用 Kadane 算法直接求解;

  2. 最大子数组跨首尾:此时最大和 = 数组总和 - 最小子数组的和(因为总和减去中间一段最小的子数组,剩下的就是跨首尾的最大子数组)。

还有一个特殊情况:如果数组中所有元素都是负数,那么"总和 - 最小子数组和"会得到 0(因为总和 = 最小子数组和),但题目要求子数组非空,所以此时直接返回常规最大和(即数组中最大的那个负数)。

代码解析(TypeScript)

typescript 复制代码
function maxSubarraySumCircular_1(nums: number[]): number {
  if (nums.length === 0) return 0;
  let curMax = nums[0], maxSum = nums[0]; // 常规最大和相关
  let curMin = nums[0], minSum = nums[0]; // 常规最小和相关
  let totalSum = nums[0]; // 数组总和

  for (let i = 1; i < nums.length; i++) {
    // 常规Kadane算法求最大子数组和
    curMax = Math.max(nums[i], curMax + nums[i]);
    maxSum = Math.max(maxSum, curMax);

    // 同理,求最小子数组和(Kadane算法变种)
    curMin = Math.min(nums[i], curMin + nums[i]);
    minSum = Math.min(minSum, curMin);

    // 累加计算数组总和
    totalSum += nums[i];
  }

  // 特殊情况:所有元素都是负数,直接返回最大和(非空)
  if (maxSum < 0) {
    return maxSum;
  }

  // 两种情况取最大值:常规最大和 vs 总和 - 最小子数组和
  return Math.max(maxSum, totalSum - minSum);
};

关键细节

  • curMax 和 curMin 分别记录"以当前元素结尾的最大子数组和"和"以当前元素结尾的最小子数组和",每次迭代更新;

  • totalSum 必须在迭代中累加,避免二次遍历数组,保证时间复杂度 O(n);

  • 判断 maxSum < 0 是核心容错,避免所有元素为负时返回 0(不符合非空子数组要求)。

解法二:前缀和 + 后缀枚举(避免总和为负的判断)

核心原理

这种解法的思路是"拆分环形子数组":跨首尾的子数组可以拆分为「前缀子数组」(从 0 开始)和「后缀子数组」(到 n-1 结束)。我们可以:

  1. 先计算常规的最大子数组和(不跨首尾);

  2. 再计算"后缀子数组 + 前缀子数组"的最大和:用 leftMax 数组记录「从 0 到 i 的最大前缀和」,再从右到左枚举后缀子数组,每次将后缀和与 leftMax[i-1](前 i-1 个元素的最大前缀和)相加,取最大值。

这种方法不需要判断数组是否全为负,因为枚举的后缀和 + 前缀和都是非空的,且常规最大和已经覆盖了全负的情况。

代码解析(TypeScript)

typescript 复制代码
function maxSubarraySumCircular_2(nums: number[]): number {
  let n: number = nums.length;
  // leftMax[i]:从0开始,到i为止的最大前缀和(必须包含0,保证前缀非空)
  const leftMax = new Array(n).fill(0);
  leftMax[0] = nums[0]; // 初始值:只有第一个元素的前缀和
  let leftSum: number = nums[0]; // 累加前缀和
  let pre: number = nums[0]; // 常规最大子数组和的中间变量(Kadane)
  let res: number = nums[0]; // 最终结果,初始化为第一个元素

  // 第一次遍历:计算常规最大和 + leftMax数组
  for (let i = 1; i < n; i++) {
    // 常规Kadane算法求最大子数组和
    pre = Math.max(pre + nums[i], nums[i]);
    res = Math.max(res, pre);

    // 累加前缀和,更新leftMax(保证leftMax[i]是0到i的最大前缀和)
    leftSum += nums[i];
    leftMax[i] = Math.max(leftMax[i - 1], leftSum);
  }

  // 第二次遍历:从右到左枚举后缀子数组,计算后缀和 + 对应最大前缀和
  let rightSum = 0;
  for (let i = n - 1; i > 0; i--) {
    rightSum += nums[i]; // 后缀和:从i到n-1的和
    // 后缀和(i到n-1) + 前缀和(0到i-1的最大),更新结果
    res = Math.max(res, rightSum + leftMax[i - 1]);
  }

  return res;
};

关键细节

  • leftMax 数组的核心作用:记录"以 0 为起点,到 i 为止"的最大前缀和,确保后续枚举后缀时,能快速找到对应的最大前缀;

  • 第二次遍历从 n-1 到 1(不包含 0),因为当 i=0 时,leftMax[i-1] 越界,且此时后缀和就是整个数组,已经被常规最大和覆盖;

  • 时间复杂度依然是 O(n),空间复杂度 O(n)(leftMax 数组),相比解法一多了一点空间,但避免了总和为负的判断,逻辑更简洁。

两种解法对比

解法 时间复杂度 空间复杂度 核心优势 适用场景
解法一(总和 - 最小和) O(n) O(1) 空间最优,逻辑直观 追求空间效率,能记住"全负判断"的场景
解法二(前缀+后缀) O(n) O(n) 无需特殊判断,逻辑更简洁 不想处理边界条件,追求代码简洁

刷题总结

环形子数组的最大和,本质是"常规子数组"和"跨首尾子数组"的最大值求解。两种解法都基于 Kadane 算法的延伸,核心是找到"跨首尾子数组"的等价转换方式------要么用总和减去最小子数组和,要么拆分为前缀+后缀。

刷题时可以根据自己的习惯选择:如果喜欢空间最优,优先解法一;如果怕遗漏边界条件,解法二更友好。另外,建议多动手模拟几个测试用例(比如全负数组、全正数组、混合数组),就能彻底掌握两种解法的逻辑。

相关推荐
炫饭第一名2 小时前
从前端视角解读 OpenClaw(上):Lit 驱动的 AI 控制网关面板
前端·人工智能·前端框架
掘金者阿豪2 小时前
MiGPT GUI给小爱音箱装「AI 大脑」,自定义人设 + 百变音色!cpolar 内网穿透实验室第 726 个成功挑战
前端·后端
滴滴答答哒2 小时前
layui响应式表单上下结构
前端·javascript·layui
比昨天多敲两行2 小时前
C++ Lsit
开发语言·c++·算法
天问一2 小时前
Cesium 中 PointPrimitiveCollection 与 primitives 的结合使用
前端
我爱C编程2 小时前
基于OMP正交匹配追踪和稀疏字典构造的杂波谱恢复算法matlab仿真
算法·matlab·omp·正交匹配追踪·稀疏字典构造·杂波谱恢复
小村儿2 小时前
觉醒的agent:AI为何抛弃React和Vue,自创Aether框架
前端·agent·ai编程
云青黛2 小时前
ReAct(推理与行动)框架
python·算法
小J听不清2 小时前
CSS 文本样式全解析:颜色 / 对齐 / 装饰 / 缩进
前端·javascript·css·html·css3