1. 前缀和:从暴力到 O(1) 区间查询的思维跃迁
提到前缀和,很多刷题党第一反应是“模板题”,但真正到了面试现场,能一口气把前缀和变种题全写出来的人其实不多。原因很简单:前缀和本身的定义只有 4 行代码,但它延伸出的思路——哈希表配合、负数取模、二维容斥、和滑动窗口的取舍——才是真正的分水岭。
这篇文章我按“基础原理 → 单题拆解 → 变种题 → 二维扩展”的顺序,把前缀和的几类经典问题彻底过一遍。包含完整可运行的 Python 代码、复杂度分析、以及我刷题时踩过的坑。适合准备算法面试的开发者、想系统补数据结构的同学,以及任何被“连续子数组”问题折磨过的人。
先建立一个共识:前缀和数组 pre[i] 表示原数组 nums[0] 到 nums[i-1] 的和,也就是前 i 个元素的总和。有了它,任意区间 [l, r) 的和可以在 O(1) 时间内算出:
python复制# 一维前缀和模板
def build_prefix(nums):
n = len(nums)
pre = [0] * (n + 1) # pre[0] = 0,方便统一处理
for i in range(n):
pre[i + 1] = pre[i] + nums[i]
return pre
# 查询区间 [l, r) 的和
def range_sum(pre, l, r):
return pre[r] - pre[l]
为什么 pre 要多开一个位置并把 pre[0] 置为 0?因为这样查询 [0, 2) 时直接 pre[2] - pre[0],不需要特判边界,代码统一且不易出错。这是前缀和所有变种题的“地基”。
接下来的几道题,都是在“维护前缀和”的基础上,叠加哈希表优化、数学性质推导或维度扩展。掌握了这些套路,你会发现自己面对“子数组之和”类问题时,不再需要暴力枚举所有区间。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 和为 K 的子数组:哈希表优化的经典范式
2.1 题目要求与暴力解的局限
题目:给定一个整数数组 nums 和一个整数 k,统计并返回和为 k 的连续子数组的个数。
暴力解法很直观:枚举所有区间 [l, r],用前缀和 O(1) 算出每个区间的和,检查是否等于 k。双重循环的复杂度是 O(n^2),数组长度一旦到 10^5,必超时。
这时候需要一个关键转换:区间 [l, r) 的和等于 pre[r] - pre[l]。题目要求等于 k,即:
pre[r] - pre[l] = k,移项得到:pre[l] = pre[r] - k。
换句话说,当我们遍历到第 r 个位置时,只需要知道前面有多少个前缀和的值等于 pre[r] - k。这个“前面出现过多少次某个值”的需求,恰好是哈希表的强项。
2.2 哈希表 + 前缀和的实现
核心思路:用一个字典 count 记录每个前缀和值出现的次数,边遍历边统计。
python复制def subarraySum(nums, k):
count = {0: 1} # 前缀和为 0 出现 1 次,对应空前缀
pre = 0
ans = 0
for num in nums:
pre += num
ans += count.get(pre - k, 0)
count[pre] = count.get(pre, 0) + 1
return ans
为什么初始要放入 {0: 1}?因为如果某个子数组从下标 0 开始,比如 nums[0] + nums[1] == k,那么计算时 pre - k == 0,这个 0 必须在哈希表中能找到,否则会漏数。
顺序也很重要:先 ans += count.get(pre - k, 0),再更新 count[pre]。如果先更新再查询,会把自己当前这个位置算进去,导致子数组长度为零的“空区间”被误计,结果错误。
2.3 两个容易踩的细节坑
第一个坑是 哈希表里存的是前缀和出现的次数,不是下标列表。有些题解会存下标数组,那通常是为了求“最长子数组长度”,而本题只要求个数,存次数就够。分清“计数”和“最值”决定了数据结构的选择。
第二个坑是 数组元素可能为负。这时候前缀和不是单调递增的,双指针(滑动窗口)无法使用,只能靠哈希表。很多人一看到“连续子数组和为 k”就想用滑动窗口,但滑动窗口的前提是数组元素非负,否则窗口扩大和缩小的方向性判断会失效。所以这类题默认用前缀和 + 哈希表,是兼容负数的最稳妥方案。
3. 和被 K 整除的子数组:负数取模的坑与数学推导
3.1 问题转化与同余定理
题目:给定一个整数数组 nums 和一个整数 k,返回其中和能被 k 整除的(连续、非空)子数组的数目。
仍然是连续子数组问题,仍然可以用前缀和转换。区间 [l, r) 的和能被 k 整除,意味着:
pre[r] - pre[l] ≡ 0 (mod k),即 pre[r] ≡ pre[l] (mod k)。
所以问题变成了:在遍历前缀和的过程中,统计每个余数出现的次数。只要两个位置的前缀和对 k 取模的余数相同,它们之间的子数组和就能被 k 整除。
3.2 Python 取模的正确写法
这里的头号大坑是负数取模。在绝大多数编程语言里,-7 % 3 的结果是 -1,而不是我们数学上期望的 2(因为 -7 = 3 * (-3) + 2)。如果用错误的结果参与哈希表统计,余数会变得五花八门,导致统计错误。
Python 的 % 运算符与数学定义一致,结果始终为非负数,所以直接用 pre % k 即可。但如果你用 C++ 或 Java,需要手动处理:
cpp复制// C++ 中正确处理负数取模
int mod = ((pre % k) + k) % k;
python复制def subarraysDivByK(nums, k):
record = {0: 1} # 余数 0 出现 1 次
pre = 0
ans = 0
for num in nums:
pre += num
mod = pre % k # Python 天然非负
ans += record.get(mod, 0)
record[mod] = record.get(mod, 0) + 1
return ans
3.3 边界变种:前缀和大于等于 K 的最短子数组
这是“被 K 整除”题目的一个热门变种:找长度最小的连续子数组,使得数组和大于等于 limit。注意这里的“大于等于”与“整除”不同,它不能直接用同余定理,而需要结合前缀和的单调性。
如果数组元素全部为正数,前缀和严格递增,这个问题可以用“滑动窗口” O(n) 解决。更通用的解法是维护一个“前缀和到下标”的有序结构(如 Python 的 SortedList),对每个位置 r,二分查找第一个满足 pre[l] <= pre[r] - limit 的位置 l,更新最小长度。这样即便元素有正有负也能处理。
这个变种很容易出现在面试追问中。我见过不少候选人能默写“和为 K”的哈希表写法,但一改成“大于等于 K 求最小长度”就卡住。原因在于他们没理解哈希表存的是“值出现次数”,而求最短长度时必须存“值对应的下标”,用二分才能快速找边界。
4. 连续数组:0 和 1 的巧妙归一化
4.1 题目描述与最值问题的差异
题目:给定一个二进制数组 nums,找到含有相同数量的 0 和 1 的最长连续子数组,并返回该子数组的长度。
这道题和前两道的差别在于:前两道是计数,这道是求最长长度。计数用字典存“次数”,求最长长度要存“最早出现的下标”。
如何用前缀和表示“0 和 1 数量相等”?常规前缀和只能求总和,解决不了“相等”的问题。这里需要一个小技巧:把 0 看成 -1。这样原数组中 0 和 1 的数量相等,就等价于“所有数之和为 0”。
python复制def findMaxLength(nums):
count = {0: -1} # 前缀和为 0 最早出现在下标 -1(即一个元素都没取时)
pre = 0
max_len = 0
for i, num in enumerate(nums):
pre += 1 if num == 1 else -1
if pre in count:
max_len = max(max_len, i - count[pre])
else:
count[pre] = i # 只记录第一次出现的位置
return max_len
4.2 为什么只存第一次出现的下标
假设前缀和为 x 的位置有 i1 < i2 < i3,那么 i3 - i1 一定大于 i3 - i2。也就是说,同一前缀和值对应的区间长度,最早出现的下标搭配当前下标一定最长。所以只需要记录最早的下标即可,无需不断更新。
注意初始值 {0: -1}:前缀和为 0 最早出现在“数组开始之前”,即下标 -1。这样当前缀和第一次变回 0 时(比如数组 [0, 1]),长度为 1 - (-1) = 2,结果正确。如果初始化成 {0: 0} 会漏掉整个数组从头开始的情况。
这类“0/1 归一化”的思路非常常用。遇到两种元素计数相等的问题,把其中一种变成负数,就把相等变成了和为 0,瞬间统一到前缀和框架下。
4.3 一个常见的思维误区
有人会问:既然 0 变成 -1 了,前缀和不就可能是负数了吗?是的,但哈希表的 key 完全支持负数,所以不影响正确性。还有人会尝试用“滑动窗口”做这道题,但窗口的伸缩条件在“0 和 1 数量相等”这个非单调特性和负数前缀和的双重影响下根本不好维护,滑动窗口在这里并不合适。
5. 矩阵区域和:二维前缀和的容斥原理
5.1 从一维到二维的推广
题目:给定一个二维矩阵 matrix,计算其子矩形范围内元素的总和。
一维前缀和解决“区间和”,二维前缀和解决“区域和”。思路完全类似,只是“前缀”变成了“从左上角到当前点的矩形区域总和”。
二维前缀和 pre[i][j] 表示从 matrix[0][0] 到 matrix[i-1][j-1] 这个矩形内所有元素的和。构建公式如下:
pre[i][j] = pre[i-1][j] + pre[i][j-1] - pre[i-1][j-1] + matrix[i-1][j-1]
这个公式本质是容斥原理:左边是“上方区域”加“左方区域”,但二者重叠部分(左上区域)被加了两次,所以要减掉一次,最后再加上当前格子的值。
python复制class NumMatrix:
def __init__(self, matrix):
m, n = len(matrix), len(matrix[0])
self.pre = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(m):
for j in range(n):
self.pre[i+1][j+1] = (self.pre[i][j+1]
+ self.pre[i+1][j]
- self.pre[i][j]
+ matrix[i][j])
def sum_region(self, r1, c1, r2, c2):
return (self.pre[r2+1][c2+1] - self.pre[r1][c2+1]
- self.pre[r2+1][c1] + self.pre[r1][c1])
5.2 查询公式怎么记不容易错
查询子矩形 (r1, c1) 到 (r2, c2) 时,可以把 pre 数组的坐标理解为“边界线”。pre[r2+1][c2+1] 是右下角边界,减去上方 pre[r1][c2+1] 和左方 pre[r2+1][c1],再加上被重复减掉的左上角 pre[r1][c1]。
如果想记不住公式,就画个 2x2 的小矩阵手推三遍,这个记忆远比死记硬背牢固。
5.3 更进一步的变种:矩阵区域和检索(二维前缀和扩展)
LeetCode 1314 题“矩阵区域和”要求对每个位置,计算以它为中心、半径为 k 的矩形区域内所有元素的和。这题和上面“区域和检索”的区别是:每个位置都要算一次,如果每个位置都调用一次 sum_region,总复杂度是 O(mn),完全可以接受,前提是先建好二维前缀和。
实现时只需要注意边界裁剪:
python复制def matrix_block_sum(mat, k):
m, n = len(mat), len(mat[0])
pre = [[0]*(n+1) for _ in range(m+1)]
for i in range(m):
for j in range(n):
pre[i+1][j+1] = pre[i][j+1] + pre[i+1][j] - pre[i][j] + mat[i][j]
ans = [[0]*n for _ in range(m)]
for i in range(m):
for j in range(n):
r1, c1 = max(0, i-k), max(0, j-k)
r2, c2 = min(m-1, i+k), min(n-1, j+k)
ans[i][j] = (pre[r2+1][c2+1] - pre[r1][c2+1]
- pre[r2+1][c1] + pre[r1][c1])
return ans
这题在笔试里出现的频率很高,特别是作为“前缀和 + 边界处理”的综合考法。记住两件事:二维前缀和提前 O(mn) 建好,每个格子 O(1) 查询,整体就是 O(mn)。
6. 前缀和与差分、表达式树:易混淆概念的一并理清
6.1 前缀和与差分是一对互逆操作
“前缀和”经常和“差分”放在一起讨论。差分数组 diff[i] = nums[i] - nums[i-1],它的前缀和恰好还原出原数组。差分主要用于“频繁对区间做同样的加减操作,最后再统一查询”的场景:把区间操作变成 O(1) 的单点修改,最后求一次前缀和得到结果。
举个例子:对数组 [1, 2, 3, 4, 5] 的区间 [1, 3] 加上 10。用差分实现:diff[1] += 10,diff[4] -= 10,最后对 diff 求前缀和就得到修改后的数组。如果直接暴力修改是 O(n),差分把单次区间修改降为 O(1)。
很多初学者把前缀和本身当成一种“数据结构”,其实它更像一种预处理思想:把循环计算区间的成本前置到构建阶段,换取后续查询的高效。差分则是它的对称操作,两者是同一枚硬币的两面。
6.2 前缀、中缀、后缀表达式与“前缀”不是一回事
搜索热词里出现了“前缀中缀和后缀表达式”,这是表达式求值的内容,和本文的数组前缀和没有直接关系,但很值得顺带澄清,避免概念混淆。
- 前缀表达式(波兰式):运算符在前,如
+ 1 2。 - 中缀表达式:运算符在中间,如
1 + 2,也就是日常写法。 - 后缀表达式(逆波兰式):运算符在后,如
1 2 +。
如果你在准备算法面试,建议把“后缀表达式求值”用栈模拟一遍,这个很容易考。但注意,它和“前缀和数组”是两个完全不同的“前缀”含义,千万别在面试时说错。
6.3 前缀和连续子数组问题的通用判断框架
最后我总结一个自己常用的判断框架:遇到“连续子数组 + 和/差/整除”相关的问题,先问三个问题:
- 数组元素是否全为非负数? 是,且求“和 >= limit 的最短长度”等最值问题,可以考虑滑动窗口;否则优先前缀和。
- 求的是个数还是最长长度? 个数,哈希表存值出现次数;最长长度,哈希表存值最早出现下标。
- 是否涉及“等于 k”或“被 k 整除”? 等于 k,哈希表查询 pre - k;被 k 整除,哈希表查询 pre % k,注意负数取模。
这套框架能覆盖绝大多数“前缀和”变种题。当你把“为什么存次数”“为什么存下标”“为什么要处理负数取模”这三个问题想透了,前缀和这一关就算真正打通了。
