当前位置: 首页 > news >正文

前缀和与差分数组——从O(n²)到O(1)的降维打击

前缀和是面试中"最被低估"的优化技巧。预计算 O(n) + 查询 O(1) 的经典模式,掌握后能解决几乎所有"区间和"类问题。

一、引言

🤔思考:面试官给你一个数组,要你快速回答"索引 2 到 7 的和是多少"。你写了一个循环累加,O(n) 搞定。面试官说:"我有 100 万次这样的查询,怎么办?" 你的 O(n) × 100 万 = 1000 亿次操作,显然不行。

这就是前缀和(Prefix Sum)要解决的问题。

前缀和是一种预计算优化技巧:先花 O(n) 时间预处理一个前缀和数组,然后每次区间和查询只需要 O(1) 时间。这是典型的空间换时间——用 O(n) 的额外空间,把查询从 O(n) 降到 O(1)。

本文围绕两个经典题目展开:

  1. 303. 区域和检索 - 数组不可变(Easy)—— 前缀和的入门模板,展示静态前缀和的核心用法

  2. 560. 和为K的子数组(Medium)—— 前缀和 + 哈希表优化,从静态预计算到动态统计的进阶

本期是「数据结构与算法面试精讲」系列第16篇。

二、前缀和基础

2.1 什么是前缀和?

前缀和(Prefix Sum)是指数组从开头到某个位置的所有元素之和。

对于数组nums,定义前缀和数组prefix,其中prefix[i]表示nums[0]nums[i-1]的和(即不包含nums[i]本身):

prefix[i] = sum(nums[0..i-1])

这种定义方式(左闭右开)的好处是:prefix[0] = 0,边界处理更统一。

2.2 前缀和的计算与使用

计算前缀和数组:

prefix[0] = 0 for i in 1..n: prefix[i] = prefix[i-1] + nums[i-1]

使用前缀和查询区间和:

sumRange(left, right) = prefix[right+1] - prefix[left]

查询示例:sumRange(1, 3)=nums[1] + nums[2] + nums[3]= 2 + 3 + 4 = 9 通过前缀和:prefix[3+1] - prefix[1]=prefix[4] - prefix[1]= 10 - 1 = 9 ✅

2.3 暴力 vs 前缀和对比

算法

预处理时间

单次查询时间

空间

暴力枚举

O(1)

O(n)

O(1)

前缀和

O(n)

O(1)

O(n)

关键洞察:当查询次数远多于数组长度时,前缀和的效果最显著。一次预计算 O(n) 的成本,被后续大量 O(1) 查询摊薄。

三、303. 区域和检索 - 数组不可变(Easy)

3.1 题目描述

给定一个整数数组nums,处理多个查询sumRange(i, j),返回数组从索引ij的元素和。

示例:

输入: nums = [-2, 0, 3, -5, 2, -1] sumRange(0, 2) → 1 (-2 + 0 + 3 = 1) sumRange(2, 5) → -1 (3 + (-5) + 2 + (-1) = -1) sumRange(0, 5) → -3 (全部元素之和 = -3)

3.2 前缀和解法

核心思路:在构造函数中预计算前缀和数组,sumRange直接查表。

class NumArray: def __init__(self, nums: List[int]): self.prefix = [0] * (len(nums) + 1) for i in range(len(nums)): self.prefix[i + 1] = self.prefix[i] + nums[i] def sumRange(self, left: int, right: int) -> int: return self.prefix[right + 1] - self.prefix[left]
class NumArray { private int[] prefix; public NumArray(int[] nums) { prefix = new int[nums.length + 1]; for (int i = 0; i < nums.length; i++) { prefix[i + 1] = prefix[i] + nums[i]; } } public int sumRange(int left, int right) { return prefix[right + 1] - prefix[left]; } }
class NumArray { private: vector<int> prefix; public: NumArray(vector<int>& nums) { prefix.resize(nums.size() + 1, 0); for (int i = 0; i < nums.size(); i++) { prefix[i + 1] = prefix[i] + nums[i]; } } int sumRange(int left, int right) { return prefix[right + 1] - prefix[left]; } };

复杂度分析:

  • 时间复杂度:初始化 O(n),查询 O(1)

  • 空间复杂度:O(n)

3.3 面试追问

追问1:如果数组很大(比如 10^9 个元素),无法全量存内存怎么办?

分段前缀和——把数组分成若干块(block),每块预计算块内和。查询时完整的块直接用块内和,不完整的块逐个累加。块大小设为sqrt(n),查询复杂度 O(√n),空间 O(√n)。

追问2:如果数组在查询之间会改变呢?

那就不能用静态前缀和。需要树状数组(Fenwick Tree)或线段树(Segment Tree),支持动态更新和区间查询。

四、560. 和为K的子数组(Medium)

4.1 题目描述

给定一个整数数组nums和一个整数k,统计该数组中和为k连续子数组的个数。

示例 1:

输入: nums = [1, 1, 1], k = 2 输出: 2 解释: [1, 1](索引0-1)和 [1, 1](索引1-2)两个子数组

示例 2:

输入: nums = [1, 2, 3], k = 3 输出: 2 解释: [1, 2](索引0-1)和 [3](索引2)

4.2 暴力解法(O(n²))

class Solution: def subarraySum(self, nums: List[int], k: int) -> int: count = 0 for i in range(len(nums)): s = 0 for j in range(i, len(nums)): s += nums[j] if s == k: count += 1 return count

nums.length达到 2×10⁴ 时,O(n²) 的 4 亿次操作必然超时。

4.3 前缀和 + 哈希表优化(O(n))

核心洞察:子数组[i+1..j]的和 =prefix[j] - prefix[i]。要统计prefix[j] - prefix[i] = k的个数,等价于遍历到j时,统计之前出现过多少个prefix[j] - k

公式推导:

子数组 [i+1..j] 的和 = k → prefix[j] - prefix[i] = k → prefix[i] = prefix[j] - k

三语言实现:

class Solution: def subarraySum(self, nums: List[int], k: int) -> int: prefix_map = {0: 1} prefix_sum = 0 count = 0 for num in nums: prefix_sum += num count += prefix_map.get(prefix_sum - k, 0) prefix_map[prefix_sum] = prefix_map.get(prefix_sum, 0) + 1 return count
class Solution { public int subarraySum(int[] nums, int k) { Map<Integer, Integer> prefixMap = new HashMap<>(); prefixMap.put(0, 1); int prefixSum = 0, count = 0; for (int num : nums) { prefixSum += num; count += prefixMap.getOrDefault(prefixSum - k, 0); prefixMap.put(prefixSum, prefixMap.getOrDefault(prefixSum, 0) + 1); } return count; } }
class Solution { public: int subarraySum(vector<int>& nums, int k) { unordered_map<int, int> prefixMap; prefixMap[0] = 1; int prefixSum = 0, count = 0; for (int num : nums) { prefixSum += num; count += prefixMap[prefixSum - k]; prefixMap[prefixSum]++; } return count; } };

复杂度分析:时间复杂度 O(n),空间复杂度 O(n)。

4.4 面试追问

追问1:如果数组包含负数怎么办?解法不变。560 题本身就支持负数。追问2:如果要求返回子数组的起始和结束位置?哈希表改存prefix_sum → [index_list]

五、差分数组简介

差分数组(Difference Array)与前缀和是"对偶"关系。前缀和解决"多次区间查询",差分数组解决"多次区间修改"。

定义:diff[i] = nums[i] - nums[i-1](其中diff[0] = nums[0]

核心操作:

  • 区间[l, r]统一加valdiff[l] += val, diff[r+1] -= val

  • 恢复数组:nums[i] = nums[i-1] + diff[i]

特性

前缀和

差分数组

解决的问题

多次区间查询

多次区间修改

核心操作

预计算 → O(1) 查询

区间修改 O(1) → 恢复

典型应用

303, 560, 304

1109, 1094

六、家族题梯度

题号

题目

难度

核心技巧

303

区域和检索

⭐ Easy

静态前缀和模板

304

二维区域和检索

⭐⭐ Medium

二维前缀和

560

和为K的子数组

⭐⭐ Medium

前缀和 + 哈希表

523

连续子数组和

⭐⭐ Medium

前缀和 + 哈希表(模)

974

和可被K整除的子数组

⭐⭐ Medium

前缀和 + 模运算

1109

航班预订统计

⭐⭐ Medium

差分数组模板

1094

拼车

⭐⭐ Medium

差分数组应用

学习建议:先做 303 和 304 掌握静态前缀和模板 → 再做 560 和 523 理解前缀和 + 哈希表 → 最后做 1109 和 1094 掌握差分数组。

参考资料

  • LeetCode 303. 区域和检索 - 数组不可变

  • LeetCode 560. 和为K的子数组

  • LeetCode 1109. 航班预订统计(差分数组典型题)

标签:前缀和, 差分数组, 算法面试, LeetCode, 数据结构

http://www.cnnetsun.cn/news/4211195.html

相关文章:

  • 快餐出海,不是把店开出去,而是把供应链搬出去!10月杭州中餐出海研讨会,快餐/简餐出海的供应链适配与本土化策略。限席!
  • 构建AI编程智能体:以DeepSeek Hermes为大脑的多工具协同工作流
  • AI大模型与数学·第49课 级数刷题集训6:泰勒级数完整实战+余项误差分析——大模型轻量化、截断近似底层数学
  • Windows局域网联机全攻略:从文件共享到游戏联机与Docker部署
  • 迷你PC构建Proxmox集群:万兆网络规划与配置实战
  • Qwen3.8本地推理性能优化实战:vLLM、量化与参数调优指南
  • 解锁大模型深度思考:Qwen2.5-72B推理调优与提示工程实战
  • 深入理解C++系列(15)——AVL树
  • AI Agent五大核心设计模式详解:从ReAct到多智能体协作
  • AI智能体工程化实战:基于LangGraph构建多智能体协作系统
  • 锤子助手第019个开关:启用左滑返回的位置、验证方法与手势冲突边界
  • Java连接MySQL数据库时“Cannot load driver class”错误的全面排查与解决方案
  • 后端开发入门:先搞懂这些核心概念再说
  • 蓝速科技圆柱形 3D 全息舱硬件选型实战指南
  • JDK 21 --enable-preview 全链路配置指南:从编译到虚拟线程落地
  • 博图PLC硬件IO自由组态:用PEEK_BOOL/POKE_BOOL突破地址刚性限制
  • 红魔8S Pro强解Bootloader与完美ROOT实战指南
  • MySQL8.0.45主从搭建传统方式以及使用mysql clone克隆方式搭建
  • 企业终端外设管控难、漏洞多?一套闭环方案彻底解决
  • C++结构体排序:重载运算符、自定义函数与Lambda表达式实战指南
  • 从E-Bench到实战:构建面向真实场景的AI Agent评测基准
  • LLM智能体恒定上下文技能学习:从状态表示到工程实践
  • LLM智能体上下文污染:重试机制中的隐蔽陷阱与解决方案
  • 多模态AI智能体如何革新电影预演:从导演意图到可视化协作决策
  • GitLab项目群组设计与权限管理:从零构建清晰可扩展的代码仓库结构
  • LLM智能体在游戏中的竞争与合作:架构、策略与工程实践
  • SnapGuard:轻量级提示词注入防御方案,为视觉Web Agent构筑安全防火墙
  • OpenClaw智能体流量镜像重构:插件化设计与性能优化实践
  • Claude生成的pdf怎么导出 加上“AI导出鸭”,效果炸裂
  • 【TDengine】MNode、VNode、QNode、SNode 各自的职责是什么?