这篇文章咱就不扯那些花里胡哨的标题了,直接聊一个我在实际开发里反复用到、看着不起眼但真能顶大用的算法组合:前缀和与差分。
如果你刷过算法题,或者工作中需要处理大量区间求和、区间增量更新的需求,八成跟它们打过照面。但很多人对这两个东西的理解停留在"背模板"的阶段——一维前缀和怎么写、二维差分怎么套,背得滚瓜烂熟,可一旦题目条件变一变、数据规模涨一涨,或者从在线评测系统换到真实业务场景,立马抓瞎。
这篇文章我不会只给代码模板。我会把这俩玩意儿背后的来龙去脉、适用边界、常见变形、还有实际工程里容易踩的坑一次说透。不管你是刚接触算法的学生,还是工作多年想补补内功的开发者,这篇文章应该都能让你对它们有个脱胎换骨的认识。
1. 先看一个业务场景:连续区间查询为什么能把服务拖垮
假设你手上有一个电商订单金额的数组,长度是 n(比如几十万量级)。运营天天要查:"第 i 天到第 j 天,总销售额是多少?" 这类查询一天可能要来几万次。
最朴素的做法是啥?每次查询都写个循环累加:
python复制def range_sum(arr, i, j):
total = 0
for k in range(i, j + 1):
total += arr[k]
return total
逻辑上没错,但性能账一算就吓人。假设数组长度 n = 100000,单次查询平均要遍历半个数组,也就是约 50000 次加法。如果一天有 10000 次查询,那就是 5 亿次加法。这还只是单机单线程的量级,一旦并发上来,CPU 直接被打满。
很多人第一反应是上缓存、上 Redis、或者加机器——但问题根源在于:你每次都在重复计算一堆已经算过的子区间。第 3 天到第 5 天的总和,跟第 4 天到第 6 天的总和,中间有大量的重叠部分,你的循环却每次都把它们从头加一遍。
这就是前缀和要解决的核心痛点:把"频繁查询"的代价从 O(n) 摊到 O(1),把计算重心挪到查询之前的预处理阶段。
我还遇到过一个更麻烦的变种场景。有一张用户行为日志表,按天统计某个事件的发生次数,得到一个数组。产品经理经常要的不是"某段区间的总和",而是"某段区间内每天的数值,都统一加上某个增量"——比如国庆大促期间,所有天数的数据都要补偿修正。如果直接用 SQL UPDATE 逐条改,几百万条记录的在线表能把你数据库连接池拖到告警。
差分的思路就是专门干这个的:用 O(1) 的时间记录一次"区间增量操作",最后再一次性把增量反映到真实数组上。下面我一个个拆开讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 一维前缀和:为什么预处理一次就能让区间查询变成 O(1)
2.1 核心定义与递推式
先说基本概念。给定一个数组 a[1..n](下文从 1 开始编号,主要是为了代码实现方便),定义前缀和数组 S:
S[i] = a[1] + a[2] + ... + a[i]
也就是第 i 个位置之前(含第 i 个)所有元素的和。这个数组怎么算?当然可以每个位置都循环一遍,但那就退化回 O(n²) 了。正确做法是利用递推关系:
S[i] = S[i-1] + a[i]
也就是说,每一项前缀和只需要看前一项的结果,再加一个当前值,一趟线性的遍历就能把整个 S 数组建出来。代码长这样(Python):
python复制def build_prefix(arr):
# arr 下标从 0 开始,prefix 下标从 0 开始,prefix[i] 表示 arr[0..i-1] 的和
n = len(arr)
prefix = [0] * (n + 1) # 多开一位,让 prefix[0] = 0,后续处理边界爽很多
for i in range(n):
prefix[i + 1] = prefix[i] + arr[i]
return prefix
def range_sum(prefix, l, r):
# 求下标 l..r 的和(arr 是原始数组),l 和 r 都是原始下标,含端点
return prefix[r + 1] - prefix[l]
这里有一个很实用的小细节:prefix 数组我开成 n + 1 长度,第 0 位补 0。这样一来,查询 [l, r] 时统一写成 prefix[r+1] - prefix[l],不需要对 l = 0 做特殊判断。这个习惯在二维前缀和、线段树合并等场景里能帮你省掉大量边界 bug。
2.2 为什么是"减一减"而不是其他做法
区间 [l, r] 的和,为什么等于 S[r] - S[l-1]?可以这么理解:前缀和 S 是一本账本,记录了从开头到某个位置的所有累计值。你想知道中间的某个段落,就需要用"到右端点的累计"减去"到左端点左边一位的累计"——这不难理解,就好比你算某个月的支出,要用"到月底的总支出"减去"到上月底的总支出"。这段内容用示意来表达会清楚得多:
a[l] + ... + a[r] = (a[1] + ... + a[r]) - (a[1] + ... + a[l-1])
左边的两个括号分别是 S[r] 和 S[l-1]。
2.3 哈希表前缀和:不只是区间求和,还能做子数组计数
前缀和另一个我在工程里用得飞起的变体是配合哈希表做"和为 target 的连续子数组计数"。经典题是 LeetCode 560 这类。
思路依然是上面那个区间和公式:一个子数组 [j+1..i] 的和等于 S[i] - S[j]。那如果我想知道有多少个子数组的和等于 target,就可以在遍历 i 的过程中,维护一个字典,记录**"历史上出现过哪些前缀和的值,每个值出现了多少次"**。每一次来到新位置 i,我查一下 S[i] - target 在字典里出现过几次,那么这些次数就对应了以 i 结尾的、和为 target 的子数组个数。
python复制def subarray_sum_count(nums, target):
prefix_sum = 0
count_map = {0: 1} # 前缀和为 0 的初始情况
ans = 0
for x in nums:
prefix_sum += x
need = prefix_sum - target
ans += count_map.get(need, 0)
count_map[prefix_sum] = count_map.get(prefix_sum, 0) + 1
return ans
这里有个关键点:初始化 count_map = {0: 1} 很重要。如果不初始化,当某个子数组正好从数组开头开始时(也就是 j = -1,相当于空前缀和 0),你永远数不到它。我见过好几个人在这里漏掉初始值,导致答案少 1。
2.4 二维前缀和:图像处理与矩阵查询的利器
如果数组变成了一张二维矩阵,你想快速求任意子矩形里的元素和怎么办?那就用二维前缀和。
定义 P[i][j] 表示从矩阵左上角 (1,1) 到 (i,j) 这个矩形区域里所有元素的和。递推公式是:
**P[i][j] = P[i-1][j] + P[i][j-
