环形子数组的最大和 图解题解
这道题到底在问什么
- 输入
- nums=[1,-2,3,-2]
- 输出
- 3 (不绕环,取 [3])
- 输入
- nums=[5,-3,5]
- 输出
- 10 (绕环,取末尾 5 接开头 5)
- 输入
- nums=[-3,-2,-3]
- 输出
- -2 (全负,只能取最大的单个)
最优解:为什么这么做
一句话答案:LeetCode 918 环形子数组的最大和用双 Kadane:答案两情形取大——不绕环是普通最大子数组和,绕环等于总和减去最小子数组和;一趟扫描同时跑最大、最小两个 Kadane,全负时特判返回最大和。时间 O(n)、空间 O(1)。
环形子数组的最大和在找什么
给一个整数数组 nums,首尾相连成环,最后一个后面接回第一个。要在环上找一段连续、非空的子数组让和最大,可从末尾绕过去接开头,每个下标最多用一次。比如 nums=[1,-2,3,-2] 不绕环取 [3] 得 3;nums=[5,-3,5] 则末尾 5 绕回接开头 5、跨过 -3,和为 10。
为什么不能把环剪断暴力枚举
把环剪断枚举所有连续段求和要 O(n²) 段,一大就吃不消。也有人复制成 2n 长再跑普通最大子数组,但得限制选段长度不超过 n,否则同一下标算两次,还要配前缀和加单调队列,繁。突破口是:环上最优段只有两种长相,分开处理一趟扫描就够。
绕环的最大,等于总和减去最小子数组
把答案分成互斥两类。第一类不绕环,就是普通连续子数组,正是最大子数组和问题(LeetCode 53):一个 Kadane 一趟扫描就能求出,每步只决定把当前元素接前段后面、还是自己另起一段,记为 bestMax。
第二类绕环,选「末尾一截 + 开头一截」,中间空一段没选。total 固定,绕环选中的和 = total 减中间那段;想让它最大,就得让挖掉那段最小。所以绕环最大和 = total 减最小子数组和,同样用 Kadane 求,只把每步 max 换成 min,记为 bestMin。
为什么两个 Kadane 一趟就能同时算完
两个 Kadane 各维护 max_end、min_end(以当前元素结尾那段的最大、最小和)和 bestMax、bestMin(至今全局最优)。它们互不干扰,一个循环扫一遍全更新。答案取 bestMax 与 total - bestMin 里更大的:前者管不绕环,后者管绕环,取大即全局最优。
拿 [5,-3,5] 亲手算一遍
total = 5 + (-3) + 5 = 7,四个量起点都设成 nums[0]=5。到 -3:最大段接前面 5 得 2 比单飞大,max_end=2、bestMax 仍 5;最小段 -3 单飞更小,min_end=-3、bestMin=-3。到第二个 5:最大段接前面 2 得 7,max_end=7、bestMax=7;最小段接上得 2 更小,min_end=2、bestMin 仍 -3。收尾 bestMax=7、bestMin=-3,答案 max(7, 7-(-3)) = 10。total 减最小段 -3,正是挖掉中间 -3、留两头两个 5 相接的和。
复杂度多少,全负数组这个坑
求 total 一趟、双 Kadane 一趟都是线性,时间 O(n);全程只用几个标量滚动,空间 O(1)。
最阴的是全负数组 nums=[-3,-2,-3]:total=-8,最小子数组是整个数组,bestMin=-8。照搬 total - bestMin = 0,等于把整个数组挖掉、剩和为 0 的空段,可题目要求非空,0 取不到。所以先看 bestMax:它 < 0 说明全负,绕环的 0 不合法,返回 bestMax(这里 -2)。另一坑是只跑最大 Kadane,绕环整类全丢,[5,-3,5] 会误答成 7。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3一句话:同时跑两个 Kadane(一个求最大段、一个求最小段),答案 = max(bestMax, total - bestMin),全负时只取 bestMax。
- 4先把整个数组的和 total=2 一次性求出来(后面是个常量);再让两个 Kadane 都从第 0 个元素 5 起步:绿色是「正在生长的最大段」,红色是「正在生长的最小段」,此刻都只含 5。
- 5看第 1 个 -2(紫)。先问绿色「最大段」:把 -2 接到前段(5)得 3,还是 -2 单飞更大?
- 6接上去更大,绿色段延伸,maxEnd=3。没超过 bestMax=5,保持。
- 7同一个 -2,换红色「最小段」视角:接上前段(5)得 3,还是 -2 单飞更小?
- 8单飞更小,红色段从 -2 重新开始,minEnd=-2。刷新 bestMin=-2。
- 9看第 2 个 3(紫)。先问绿色「最大段」:把 3 接到前段(3)得 6,还是 3 单飞更大?
- 10接上去更大,绿色段延伸,maxEnd=6。超过旧 bestMax,刷新 bestMax=6。
- 11同一个 3,换红色「最小段」视角:接上前段(-2)得 1,还是 3 单飞更小?
- 12接上去更小,红色段延伸,minEnd=1。没小过 bestMin=-2,保持。
- 13看第 3 个 -8(紫)。先问绿色「最大段」:把 -8 接到前段(6)得 -2,还是 -8 单飞更大?
- 14接上去更大,绿色段延伸,maxEnd=-2。没超过 bestMax=6,保持。
- 15同一个 -8,换红色「最小段」视角:接上前段(1)得 -7,还是 -8 单飞更小?
- 16单飞更小,红色段从 -8 重新开始,minEnd=-8。刷新 bestMin=-8。
- 17看第 4 个 4(紫)。先问绿色「最大段」:把 4 接到前段(-2)得 2,还是 4 单飞更大?
- 18单飞更大,绿色段从 4 重新开始,maxEnd=4。bestMax 仍是 6。
- 19同一个 4,换红色「最小段」视角:接上前段(-8)得 -4,还是 4 单飞更小?
- 20接上去更小,红色段延伸,minEnd=-4。没小过 bestMin=-8,保持。
- 21先看不绕环:最大子数组是绿色这段(5+-2+3 = 6)。这是候选 A。
- 22再看绕环:要绕环,就等于「挖掉中间一段、留两头」。挖掉的越小越好 → 挖掉红色这段最小子数组(和 = -8)。
- 23挖掉红段后,剩下两头(绿色)绕环接成一段:2 - (-8) = 10。这是候选 B,正是末尾绕回开头的那段。
- 24两个候选取大:B 更大,绕环胜出,答案 = 10(高亮即最终选中的那段)。
⚠️ 容易写错的地方
✗ 错:全负时返回 total - bestMin
✓ 对:bestMax < 0 时直接返回 bestMax
全负数组绕环会挖掉整个数组、剩空段(和为 0),但题目要求非空
✗ 错:只跑最大 Kadane
✓ 对:必须同时跑最小 Kadane
绕环的最大和 = total - 最小子数组和,缺了最小段就漏了绕环情况
✗ 错:把环展开成 2n 长度再做
✓ 对:双 Kadane 即可
展开 2n 还要限制窗口长度 ≤ n,更复杂;双 Kadane 一遍 O(n) 更优
完整代码(Python / C++ / Java)
Python
from typing import List
class Solution:
def maxSubarraySumCircular(self, nums: List[int]) -> int:
total = sum(nums)
max_end = min_end = nums[0]
best_max = best_min = nums[0]
for i in range(1, len(nums)):
x = nums[i]
max_end = max(x, max_end + x)
best_max = max(best_max, max_end)
min_end = min(x, min_end + x)
best_min = min(best_min, min_end)
return best_max if best_max < 0 else max(best_max, total - best_min)C++
#include <algorithm>
#include <numeric>
#include <vector>
using namespace std;
class Solution {
public:
int maxSubarraySumCircular(vector<int>& nums) {
int total = accumulate(nums.begin(), nums.end(), 0);
int maxEnd = nums[0], minEnd = nums[0], bestMax = nums[0], bestMin = nums[0];
for (int i = 1; i < (int)nums.size(); ++i) {
int x = nums[i];
maxEnd = max(x, maxEnd + x); bestMax = max(bestMax, maxEnd);
minEnd = min(x, minEnd + x); bestMin = min(bestMin, minEnd);
}
return bestMax < 0 ? bestMax : max(bestMax, total - bestMin);
}
};Java
import java.util.*;
class Solution {
public int maxSubarraySumCircular(int[] nums) {
int total = 0;
for (int x : nums) total += x;
int maxEnd = nums[0], minEnd = nums[0], bestMax = nums[0], bestMin = nums[0];
for (int i = 1; i < nums.length; i++) {
int x = nums[i];
maxEnd = Math.max(x, maxEnd + x); bestMax = Math.max(bestMax, maxEnd);
minEnd = Math.min(x, minEnd + x); bestMin = Math.min(bestMin, minEnd);
}
return bestMax < 0 ? bestMax : Math.max(bestMax, total - bestMin);
}
}复杂度
时间
O(n)
先求一遍 total,再一趟扫描同时跑两个 Kadane,都是线性
空间
O(1)
只用 maxEnd/minEnd/bestMax/bestMin/total 几个标量
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 环形子数组的最大和 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
和最大子数组和(LeetCode 53)是什么关系?+
LeetCode 53 只有不绕环一种情况,一个 Kadane 扫完就是答案。918 是它的环形版,多出绕环情形,办法是再跑一个求最小子数组和的 Kadane,用 total 减 bestMin 得到绕环的最大和,与 bestMax 取大,最后处理全负特判。会了 53,918 就是多加一条最小 Kadane 加一个边界。
为什么不干脆把数组复制成 2n 长度再求最大子数组?+
能做,但更麻烦。复制成 2n 后普通 Kadane 会选出长度超过 n 的段、把同一个下标算两次,所以必须限制窗口长度不超过 n,这就得上前缀和加单调队列,代码量和思维量都比双 Kadane 大。双 Kadane 一趟 O(n)、空间 O(1),是面试首选。
全负数组为什么一定要特判 bestMax < 0?+
绕环情形的和 = total - bestMin,它暗含「至少挖掉一段、保留剩下的」。当所有数都是负数,最优的「挖掉」就是把整个数组挖走,剩下空段和为 0,可题目要求子数组非空,这个 0 是非法解。此时 bestMax 记录的是最大的单个负数(能取到的最优非空段),所以 bestMax < 0 时直接返回 bestMax,跳过绕环这条会给出 0 的岔路。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 环形子数组的最大和 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。