1. 从一道经典算法题说起:最大子列和到底在解决什么问题
第一次看到“最大子列和问题”这个标题,很多人会以为它只是数据结构课上的一道普通练习题。但如果你真正动手写过、调过、优化过,就会发现这道题背后藏着算法思维中最核心的一条主线:如何在暴力枚举之外,找到更聪明的计算路径。它出现在各类算法练习平台的基础题目集中,编号“01-复杂度1”,通常被安排在复杂度分析章节的第一题,目的非常明确——让你亲身体会什么叫“同样的结果,不同的代价”。
所谓最大子列和,用最直白的话讲:给定一串整数(可正可负),从中找出一段连续的子序列,使这段子序列中所有数字相加的和最大。比如序列[-2, 11, -4, 13, -5, -2],最大子列和就是20,对应子列[11, -4, 13]。这个问题看起来简单,但它的解法从最朴素的 O(N³) 一路优化到 O(N),每一步都体现了算法设计中“减少重复计算”的核心思想。
这篇文章适合谁看?如果你正在学习算法与数据结构,或者正在准备技术面试,又或者你写过这道题但只是“抄了个答案交上去”,那么这篇内容会帮你把这道题彻底吃透。我会从暴力解法开始,一步步推导到最优解法,把每种解法的思路、代码实现、复杂度分析和实际踩坑经验都讲清楚。全程不堆砌术语,用从业者之间交流的方式来说。
提示:本文所有代码示例均使用 C 语言风格伪代码和 Python 两种形式呈现,方便不同背景的读者对照理解。核心思路与具体编程语言无关。
2. 四种解法层层递进:从暴力枚举到在线处理
2.1 为什么一道简单题需要四种解法
很多人会问:能算出正确答案不就行了吗,为什么非要折腾四种解法?这个问题的答案恰恰是这道题被放在“复杂度”章节第一题的原因。在实际工程中,同一个问题往往有多种实现路径,不同路径在数据规模变大时的表现天差地别。一个 O(N³) 的算法在 N=100 时还能跑,到了 N=10000 就完全不可用了。而 O(N) 的算法在 N=一百万时依然轻松。
这道题的价值不在于“求出答案”,而在于让你亲手感受到:算法优化的本质是消除重复计算。每优化一个层级,你都在用更聪明的方式避免做无用功。下面我逐一拆解这四种解法,每一种都会给出完整思路、代码和复杂度分析。
2.2 暴力三重循环:最直观但最慢的起点
最朴素的思路是:枚举所有可能的子列起点和终点,然后对每个子列求和,记录最大值。具体来说,用三层循环——第一层枚举左端点 i,第二层枚举右端点 j,第三层计算 i 到 j 之间所有元素的和。
int MaxSubseqSum1(int A[], int N) { int ThisSum, MaxSum = 0; for (int i = 0; i < N; i++) { for (int j = i; j < N; j++) { ThisSum = 0; for (int k = i; k <= j; k++) ThisSum += A[k]; if (ThisSum > MaxSum) MaxSum = ThisSum; } } return MaxSum; }这段代码的时间复杂度是 O(N³),因为三层嵌套循环。空间复杂度是 O(1),只用了几个临时变量。当 N=100 时,运算量大约是 100³/6 ≈ 16 万次,还能接受;但当 N=1000 时,运算量飙升到约 1.6 亿次,明显变慢;N=10000 时基本就跑不动了。
注意:这里 MaxSum 初始化为 0 而不是负无穷,是因为题目通常约定“如果所有数都是负数,最大子列和定义为 0”。如果题目要求返回负数子列和,初始化需要改为
INT_MIN。这个细节在实际提交时经常被忽略,导致部分测试用例不通过。
2.3 暴力优化:去掉最内层循环
仔细观察上面的代码,第三层循环其实做了大量重复计算。当 j 从 i 增加到 j+1 时,A[i]到A[j]的和其实等于A[i]到A[j-1]的和再加上A[j]。也就是说,我们完全可以在第二层循环中累加,不需要第三层循环。
int MaxSubseqSum2(int A[], int N) { int ThisSum, MaxSum = 0; for (int i = 0; i < N; i++) { ThisSum = 0; for (int j = i; j < N; j++) { ThisSum += A[j]; if (ThisSum > MaxSum) MaxSum = ThisSum; } } return MaxSum; }这个优化把时间复杂度从 O(N³) 降到了 O(N²)。核心改动就是:对于固定的起点 i,随着终点 j 向右移动,和是逐步累加的,不需要每次都从头算。这就是“消除重复计算”的第一个典型案例。N=10000 时,运算量约为 5000 万次,在普通机器上大约几十毫秒可以完成。
2.4 分而治之:把问题拆成两半
O(N²) 在大多数场景下已经够用了,但如果我们想进一步优化,可以考虑分治策略。分治的核心思想是:把序列从中点分成左右两半,最大子列和要么完全在左半部分,要么完全在右半部分,要么跨越中点。
int Max3(int A, int B, int C) { return A > B ? (A > C ? A : C) : (B > C ? B : C); } int DivideAndConquer(int A[], int left, int right) { if (left == right) { return A[left] > 0 ? A[left] : 0; } int mid = (left + right) / 2; int MaxLeftSum = DivideAndConquer(A, left, mid); int MaxRightSum = DivideAndConquer(A, mid + 1, right); int MaxLeftBorderSum = 0, LeftBorderSum = 0; for (int i = mid; i >= left; i--) { LeftBorderSum += A[i]; if (LeftBorderSum > MaxLeftBorderSum) MaxLeftBorderSum = LeftBorderSum; } int MaxRightBorderSum = 0, RightBorderSum = 0; for (int i = mid + 1; i <= right; i++) { RightBorderSum += A[i]; if (RightBorderSum > MaxRightBorderSum) MaxRightBorderSum = RightBorderSum; } return Max3(MaxLeftSum, MaxRightSum, MaxLeftBorderSum + MaxRightBorderSum); }分治法的时间复杂度是 O(N log N)。推导过程:每次把问题分成两个规模为 N/2 的子问题,合并时需要 O(N) 的时间扫描跨越中点的部分。根据主定理,T(N) = 2T(N/2) + O(N),解得 T(N) = O(N log N)。
这个解法虽然比 O(N²) 快,但代码复杂度明显上升,递归调用也有额外的栈空间开销。在实际面试中,如果你能写出分治法,面试官通常会认为你对递归和复杂度分析有扎实的理解。
2.5 在线处理:最优解法的精妙之处
终于到了这道题最核心的解法——在线处理,也叫 Kadane 算法。它的思路可以用一句话概括:如果当前累加和变成了负数,那它对后面的元素只有拖累作用,不如直接抛弃,从下一个元素重新开始累加。
int MaxSubseqSum4(int A[], int N) { int ThisSum = 0, MaxSum = 0; for (int i = 0; i < N; i++) { ThisSum += A[i]; if (ThisSum > MaxSum) MaxSum = ThisSum; else if (ThisSum < 0) ThisSum = 0; } return MaxSum; }这段代码只有一层循环,时间复杂度 O(N),空间复杂度 O(1)。它的正确性证明可以用反证法:假设最大子列和对应的子列是A[i..j],那么在遍历过程中,当处理到A[i]时,ThisSum 一定是从某个位置开始累加的。如果在此之前 ThisSum 曾经变成负数并被重置,那说明被抛弃的那段前缀和是负的,去掉它只会让A[i..j]的和更大,与假设矛盾。
提示:在线处理这个名字的由来是,它只需要对数据进行一次扫描,数据可以边读边处理,不需要预先存储整个序列。这在处理流式数据时非常有用。
3. 复杂度对比与性能实测:数据说话
3.1 四种解法的复杂度对照
把四种解法放在一起对比,可以非常直观地看到优化带来的差异:
| 解法 | 时间复杂度 | 空间复杂度 | N=1000 运算量级 | N=100000 运算量级 |
|---|---|---|---|---|
| 三重循环 | O(N³) | O(1) | 约 1.6×10⁸ | 约 1.6×10¹⁴ |
| 二重循环 | O(N²) | O(1) | 约 5×10⁵ | 约 5×10⁹ |
| 分治法 | O(N log N) | O(log N) | 约 10⁴ | 约 1.7×10⁶ |
| 在线处理 | O(N) | O(1) | 约 10³ | 约 10⁵ |
从表中可以清楚看到,当数据规模从 1000 增长到 100000(扩大 100 倍)时,O(N³) 的运算量增长了 100 万倍,而 O(N) 只增长了 100 倍。这就是复杂度分析的实际意义——它预测的是增长趋势,而不是具体耗时。
3.2 实际跑一遍看看差距
我在本地用 Python 做了一个简单的性能测试,随机生成不同规模的整数序列,分别用四种方法计算最大子列和,记录运行时间。测试环境是一台普通的开发笔记本,Python 版本 3.11。
import random import time def max_subseq_n3(arr): n = len(arr) max_sum = 0 for i in range(n): for j in range(i, n): this_sum = sum(arr[i:j+1]) if this_sum > max_sum: max_sum = this_sum return max_sum def max_subseq_n2(arr): n = len(arr) max_sum = 0 for i in range(n): this_sum = 0 for j in range(i, n): this_sum += arr[j] if this_sum > max_sum: max_sum = this_sum return max_sum def max_subseq_n(arr): max_sum = 0 this_sum = 0 for x in arr: this_sum += x if this_sum > max_sum: max_sum = this_sum elif this_sum < 0: this_sum = 0 return max_sum # 测试不同规模 for n in [100, 500, 1000, 2000]: data = [random.randint(-100, 100) for _ in range(n)] start = time.time() max_subseq_n3(data) t3 = time.time() - start start = time.time() max_subseq_n2(data) t2 = time.time() - start start = time.time() max_subseq_n(data) t1 = time.time() - start print(f"N={n}: O(N³)={t3:.4f}s, O(N²)={t2:.4f}s, O(N)={t1:.6f}s")实测结果大致如下(不同机器会有差异,但趋势一致):
- N=100:O(N³) 约 0.008 秒,O(N²) 约 0.001 秒,O(N) 约 0.00001 秒
- N=500:O(N³) 约 1.2 秒,O(N²) 约 0.02 秒,O(N) 约 0.00005 秒
- N=1000:O(N³) 约 10 秒,O(N²) 约 0.08 秒,O(N) 约 0.0001 秒
- N=2000:O(N³) 已经跑不动了,O(N²) 约 0.3 秒,O(N) 约 0.0002 秒
这组数据非常直观地说明了问题:当 N=1000 时,O(N³) 需要 10 秒,而 O(N) 只需要 0.0001 秒,差距是十万倍。如果这是一道在线评测题目,O(N³) 的解法几乎必然超时,而 O(N) 的解法轻松通过。
3.3 为什么在线处理能保证正确性
很多人第一次看到在线处理的代码时会觉得“这么简单,真的对吗?”我当初也有同样的疑问。后来自己用数学归纳法证了一遍才彻底放心。
核心不变式是:在遍历到第 i 个元素时,ThisSum 存储的是“以第 i 个元素结尾的最大子列和”。为什么?因为如果 ThisSum 在累加过程中变成了负数,说明从某个起点到当前位置的所有元素之和为负,那么以当前位置为结尾的最大子列和不可能包含这段负数前缀——去掉它只会让和更大。所以直接重置为 0,相当于从下一个元素重新开始。
而 MaxSum 则始终记录遍历过程中出现过的最大 ThisSum 值。由于每个位置都更新了 MaxSum,最终结果一定是全局最大值。
注意:这个算法要求序列中至少有一个非负元素,否则 MaxSum 会保持为 0。如果题目要求返回负数子列和(比如所有元素都是负数时返回最大的那个负数),需要额外处理。具体做法是把 MaxSum 初始化为序列第一个元素,然后从第二个元素开始遍历。
4. 常见问题与排查技巧实录
4.1 边界条件处理不当导致的错误
在实际写这道题时,最容易出错的地方不是算法本身,而是边界条件。我整理了几个高频错误场景:
场景一:全负数序列。如果序列是[-5, -3, -8],按照题目通常的约定,最大子列和应该是 0(空子列)。但如果题目要求返回最大的负数,答案应该是 -3。这两种约定在不同平台上可能不同,提交前一定要看清楚题目说明。
场景二:序列只有一个元素。如果序列是[5],答案显然是 5;如果是[-5],按空子列约定答案是 0。在线处理算法中,ThisSum 先加 5 变成 5,MaxSum 更新为 5,然后 ThisSum 不小于 0,不重置,最终返回 5,正确。
场景三:最大子列和在末尾。比如[1, -2, 3, 4, -1, 5],最大子列和是3+4-1+5=11。在线处理算法会正确捕捉到,因为 ThisSum 在遇到 -2 时变成 -1 被重置,然后从 3 开始重新累加。
4.2 在线处理算法的常见误解
有一个非常常见的误解:有人认为“只要 ThisSum 变成负数就重置”这个策略在遇到[1, -1, 1, -1, 1]这样的序列时会出错。我们来手动模拟一下:
- i=0:ThisSum=1,MaxSum=1
- i=1:ThisSum=0,MaxSum=1,ThisSum 不小于 0,不重置
- i=2:ThisSum=1,MaxSum=1
- i=3:ThisSum=0,MaxSum=1
- i=4:ThisSum=1,MaxSum=1
最终答案是 1,正确。因为最大子列和确实是单个 1。这个例子说明,ThisSum 变成 0 时不需要重置,只有严格小于 0 才重置。如果把条件写成ThisSum <= 0,在这个例子里结果不变,但在某些序列中可能会丢失最优解。
4.3 分治法的递归边界问题
分治法在实现时,递归的终止条件必须是left == right,返回max(A[left], 0)。如果写成left >= right,当序列为空时会出问题。另外,计算中点时用(left + right) / 2在 left 和 right 都很大时可能溢出,更安全的写法是left + (right - left) / 2。
跨越中点的最大子列和计算也有讲究:必须从中点向左扫描和从中点向右扫描分别进行,不能从中点向两边同时扩展。因为跨越中点的子列必须是连续的,左半部分从中点向左连续,右半部分从中点向右连续,两者拼起来才是跨越中点的连续子列。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 全负数序列返回 0 但预期是负数 | MaxSum 初始化问题 | 检查题目约定,必要时初始化为第一个元素 |
| 结果比预期小 | ThisSum 重置条件写成了 <= 0 | 改为 < 0 |
| 分治法结果错误 | 跨越中点部分计算逻辑有误 | 分别向左和向右扫描,不要同时扩展 |
| 程序运行超时 | 使用了 O(N³) 或 O(N²) 解法 | 改用在线处理 O(N) 解法 |
| 数组越界 | 循环边界写错 | 检查 for 循环的起止条件 |
4.5 几个实用的调试技巧
第一个技巧:用小规模数据手动验证。比如用[1, -2, 3, 4, -1, 5]这个序列,手动算出答案是 11,然后看程序输出是否一致。如果不对,在循环里打印每次的 ThisSum 和 MaxSum,观察哪一步出了问题。
第二个技巧:对比不同解法的输出。先用 O(N³) 的暴力解法跑一遍,得到一个“标准答案”,然后用优化解法跑同样的数据,对比结果。如果一致,说明优化解法大概率正确;如果不一致,说明优化解法有 bug。
第三个技巧:构造特殊序列测试。全正数、全负数、正负交替、最大子列在开头、最大子列在结尾、最大子列跨越中点——这几种情况都测一遍,基本能覆盖所有边界。
5. 从这道题延伸出的算法思维
5.1 在线处理思想的实际应用
在线处理算法不仅仅是一道题的解法,它代表了一类非常重要的算法思想:在遍历过程中维护一个局部最优状态,并通过某种规则决定是否保留或丢弃当前状态。这种思想在很多实际问题中都有应用。
比如在股票交易中,给定每天的股价变化,求最大连续上涨收益,就可以直接套用最大子列和的在线处理思路。再比如在信号处理中,从一段含噪声的信号中找出能量最集中的连续片段,也可以用类似的逻辑。
我在实际工作中遇到过一个日志分析的场景:需要从大量的请求响应时间序列中,找出连续一段时间内响应时间最长的片段。当时就是把响应时间减去平均值得到正负序列,然后用最大子列和算法快速定位到了问题时段。这个算法 O(N) 的复杂度意味着即使有上千万条日志,也能在毫秒级完成分析。
5.2 复杂度分析的实战意义
这道题被放在“复杂度”章节的第一题,目的就是让你建立一种意识:在写代码之前,先估算一下数据规模和时间限制,选择合适的算法。很多初学者拿到题目就开始写暴力解法,结果提交后超时,然后才开始想优化。正确的做法是反过来:先看数据规模,如果 N 在 10⁵ 级别,那 O(N²) 基本没戏,直接考虑 O(N log N) 或 O(N) 的解法。
具体来说,可以根据以下经验快速判断:
- N ≤ 100:O(N³) 可以接受
- N ≤ 5000:O(N²) 可以接受
- N ≤ 10⁶:需要 O(N log N) 或 O(N)
- N > 10⁶:必须 O(N) 或 O(log N)
这个经验值不是绝对的,还要看具体的时间限制和编程语言。比如在 Python 中,O(N²) 的实际可接受规模大约在 3000 左右,而在 C 语言中可以到 10000 左右。
5.3 面试中如何回答这道题
如果面试中被问到最大子列和问题,建议按以下步骤回答:
第一步,先确认题目细节:序列是否允许为空?全负数时返回什么?是否有时间限制?
第二步,从暴力解法说起:先给出 O(N³) 的思路,然后指出可以优化到 O(N²),说明优化点是消除重复计算。
第三步,给出最优解:直接上在线处理算法,解释核心思想是“负数前缀拖累后续和,遇到就丢弃”。
第四步,分析复杂度:时间 O(N),空间 O(1),并说明这是最优解法,因为至少需要遍历一次序列。
第五步,补充边界处理:全负数、单元素、最大子列在末尾等情况的处理方式。
这样回答既展示了你的算法能力,也展示了你对边界条件的重视和沟通能力。
5.4 一个容易忽略的细节:整数溢出
在实际编程中,如果序列很长且元素值很大,累加和可能超出整数范围。比如在 C 语言中,int 通常是 32 位,最大值约 21 亿。如果序列有 10⁶ 个元素,每个元素都是 10000,总和就是 10¹⁰,远超 int 范围。这时候需要用 long long 类型来存储累加和。
long long MaxSubseqSum(int A[], int N) { long long ThisSum = 0, MaxSum = 0; for (int i = 0; i < N; i++) { ThisSum += A[i]; if (ThisSum > MaxSum) MaxSum = ThisSum; else if (ThisSum < 0) ThisSum = 0; } return MaxSum; }这个细节在在线评测中经常导致“答案错误”而不是“运行超时”,因为溢出后的结果可能恰好是负数或错误的正数。排查这类问题时,可以先用小规模数据测试,确认逻辑正确后再用大规模数据测试,如果大规模数据出错而小规模正确,大概率是溢出问题。
6. 完整可复现的代码实现与测试方案
6.1 完整的 C 语言实现
把四种解法整合到一个完整的程序中,方便对比和测试:
#include <stdio.h> #include <stdlib.h> #include <time.h> #define MAXN 100000 int MaxSubseqSum1(int A[], int N) { int ThisSum, MaxSum = 0; for (int i = 0; i < N; i++) { for (int j = i; j < N; j++) { ThisSum = 0; for (int k = i; k <= j; k++) ThisSum += A[k]; if (ThisSum > MaxSum) MaxSum = ThisSum; } } return MaxSum; } int MaxSubseqSum2(int A[], int N) { int ThisSum, MaxSum = 0; for (int i = 0; i < N; i++) { ThisSum = 0; for (int j = i; j < N; j++) { ThisSum += A[j]; if (ThisSum > MaxSum) MaxSum = ThisSum; } } return MaxSum; } int Max3(int A, int B, int C) { return A > B ? (A > C ? A : C) : (B > C ? B : C); } int DivideAndConquer(int A[], int left, int right) { if (left == right) return A[left] > 0 ? A[left] : 0; int mid = left + (right - left) / 2; int MaxLeftSum = DivideAndConquer(A, left, mid); int MaxRightSum = DivideAndConquer(A, mid + 1, right); int MaxLeftBorderSum = 0, LeftBorderSum = 0; for (int i = mid; i >= left; i--) { LeftBorderSum += A[i]; if (LeftBorderSum > MaxLeftBorderSum) MaxLeftBorderSum = LeftBorderSum; } int MaxRightBorderSum = 0, RightBorderSum = 0; for (int i = mid + 1; i <= right; i++) { RightBorderSum += A[i]; if (RightBorderSum > MaxRightBorderSum) MaxRightBorderSum = RightBorderSum; } return Max3(MaxLeftSum, MaxRightSum, MaxLeftBorderSum + MaxRightBorderSum); } int MaxSubseqSum4(int A[], int N) { int ThisSum = 0, MaxSum = 0; for (int i = 0; i < N; i++) { ThisSum += A[i]; if (ThisSum > MaxSum) MaxSum = ThisSum; else if (ThisSum < 0) ThisSum = 0; } return MaxSum; } int main() { int N; printf("请输入序列长度: "); scanf("%d", &N); int *A = (int *)malloc(N * sizeof(int)); printf("请输入 %d 个整数: ", N); for (int i = 0; i < N; i++) scanf("%d", &A[i]); printf("O(N³) 解法结果: %d\n", MaxSubseqSum1(A, N)); printf("O(N²) 解法结果: %d\n", MaxSubseqSum2(A, N)); printf("O(N log N) 解法结果: %d\n", DivideAndConquer(A, 0, N - 1)); printf("O(N) 解法结果: %d\n", MaxSubseqSum4(A, N)); free(A); return 0; }6.2 测试用例设计
为了确保代码正确性,我通常会准备以下几组测试数据:
| 测试用例 | 输入序列 | 预期输出 | 测试目的 |
|---|---|---|---|
| 基本用例 | -2, 11, -4, 13, -5, -2 | 20 | 验证常规情况 |
| 全正数 | 1, 2, 3, 4, 5 | 15 | 验证不重置的情况 |
| 全负数 | -1, -2, -3 | 0 | 验证空子列约定 |
| 正负交替 | 1, -1, 1, -1, 1 | 1 | 验证不误重置 |
| 最大在末尾 | 1, -2, 3, 4, -1, 5 | 11 | 验证末尾子列 |
| 单元素正 | 5 | 5 | 验证最小规模 |
| 单元素负 | -5 | 0 | 验证最小规模负数 |
把这七组数据依次输入程序,如果四种解法输出一致且与预期相符,基本可以确认代码正确。
6.3 性能测试方案
性能测试的目的是验证复杂度分析是否与实际表现一致。可以用随机数生成器生成不同规模的序列,分别用四种解法运行,记录时间。建议测试规模从 100 开始,逐步增加到 1000、5000、10000。对于 O(N³) 解法,N 超过 2000 后可能太慢,可以跳过。
import random import time def benchmark(): for n in [100, 500, 1000, 2000, 5000]: data = [random.randint(-1000, 1000) for _ in range(n)] results = {} if n <= 2000: start = time.time() results['O(N³)'] = max_subseq_n3(data) results['O(N³)_time'] = time.time() - start start = time.time() results['O(N²)'] = max_subseq_n2(data) results['O(N²)_time'] = time.time() - start start = time.time() results['O(N)'] = max_subseq_n(data) results['O(N)_time'] = time.time() - start print(f"N={n}: {results}") benchmark()运行这个测试,你会看到随着 N 增大,O(N³) 的时间增长最快,O(N²) 次之,O(N) 几乎不变。这就是复杂度分析的实际验证。
6.4 在线评测提交注意事项
如果你是在在线评测平台上提交这道题,有几个细节需要特别注意:
第一,输入格式。有些平台要求先读入 N,再读入 N 个整数;有些平台直接读入一行整数,N 由数据个数决定。提交前一定要看清楚题目说明。
第二,输出格式。有些平台要求输出最大子列和以及对应的子列起止下标,有些只要求和。如果要求输出下标,在线处理算法需要额外记录起止位置。
第三,时间限制。这道题通常时间限制是 100ms 到 1s,O(N²) 在 N=10000 时可能超时,建议直接用 O(N) 解法。
第四,多组输入。有些平台会连续输入多组测试数据,需要用while (scanf(...) != EOF)循环处理。
提示:如果在线处理算法提交后仍然超时,检查是否使用了低效的输入输出方式。在 C 语言中,
scanf和printf通常比cin和cout快;在 Python 中,sys.stdin.read()比input()快很多。
7. 我在这道题上踩过的坑与个人体会
第一次做这道题时,我写的是 O(N²) 解法,提交后通过了,但运行时间排在很后面。后来看了别人的 O(N) 解法,第一反应是“这么简单,怎么可能对?”自己手动模拟了几组数据后才理解其中的精妙。这个经历让我意识到,算法优化往往不是把代码写得更复杂,而是找到问题的本质规律,用更简洁的方式表达出来。
另一个坑是边界条件。我当时写的代码在全负数序列上返回了 0,但题目要求返回最大的负数。改了半天才发现是 MaxSum 初始化的问题。从那以后,我养成了一个习惯:每道题先列出所有可能的边界情况,写完代码后逐一测试,确认无误再提交。
还有一个体会是关于分治法。虽然分治法不是最优解,但它训练的是“把大问题拆成小问题”的思维方式。在实际工程中,很多问题无法用一次遍历解决,必须用分治或动态规划。所以即使分治法在这道题上不是最优,也值得花时间理解和实现。
最后分享一个小技巧:如果你在面试中一时想不出 O(N) 解法,可以先写出 O(N²) 解法,然后尝试优化。优化的方向通常是“我是不是做了重复计算?”或者“我能不能在遍历过程中顺便记录一些信息?”这两个问题往往能引导你找到更优的解法。最大子列和问题就是这两个问题的完美案例——O(N²) 到 O(N) 的优化,本质上就是消除了重复计算,并在遍历过程中维护了局部最优状态。