[百题重刷]前缀和 + Hash 表:缓存思想, 消除重复计算

问题引入:

给定一个数组和一个目标值 k,求有多少个子数组的和等于 k?

比如数组 [1, 1, 1],k = 2,答案是 2, 有两个子数组 [1,1] 的和为 2。

最直观的想法: 暴力求解

python 复制代码
def subarraySum_bruteforce(nums, k):
    count = 0
    n = len(nums)
    # 枚举所有子数组
    for i in range(n):
        for j in range(i, n):
            # 计算 nums[i:j+1] 的和
            subarray_sum = sum(nums[i:j+1])
            if subarray_sum == k:
                count += 1
    return count

这个方法的问题很明显:O(n³) 的时间复杂度。当数组有 10, 000 个元素时,提交后 LC 应该就会报超时了。

优化一下

不再每次都调用 sum(nums[i:j+1])(O(n) 的操作),而是用一个变量 current_sum 逐步累加。

python 复制代码
def subarraySum_better(nums, k):
    count = 0
    n = len(nums)
    for i in range(n):
        current_sum = 0
        for j in range(i, n):
            current_sum += nums[j]  # 累加,避免重复计算
            if current_sum == k:
                count += 1
    return count

好一点了,现在是 O(n²)。但对于大数据集,这仍然不够快。


基于前缀和的优化思路

还能不能再进行优化下, 让这个问题可以在 O(n) 时间内解决,仍然有很严重的重复计算的问题
如:

python 复制代码
for i in range(n):
    current_sum = 0
    for j in range(i, n):
        current_sum += nums[j]  # 这一行做了大量重复计算!

看看一个例子,数组是 [1, 2, 3]

复制代码
i=0 时:
  j=0: 1
  j=1: 1 + 2       ← 1 又加了一遍
  j=2: 1 + 2 + 3   ← 1 和 2 又加了一遍

i=1 时:
  j=1: 2
  j=2: 2 + 3       ← 2 又加了一遍

在这个过程中,1 被加了 2 次,2 被加了 2 次,3 被加了 1 次。 同一个数字一直在被重复加! 所以我们发现了可以优化的问题和空间: 在计算过程中存在重复的加法计算。明明 1 + 2 已经算过了,结果到了 j=2 又要再加一遍 1 和 2。

能不能把计算过的结果缓存下来, 下次使用时直接从缓存读取, 避免重复呢?

这里就引入了经典的技巧: 前缀和, 如果我们能提前把所有的"前缀和"计算并缓存起来 ,就不用每次都重新累加了!

这就是前缀和 的核心思想:与其每次都从头开始累加,不如一开始就把"从开头到每个位置的累加和"全部缓存下来。这样,计算任意子数组的和就变成了一个简单的减法,完全避免了重复累加!

什么是前缀和?

前缀和就是"从数组开头到当前位置的累加和"。

复制代码
数组:        [1,  2,  3,  4,  5]
前缀和:  [0,  1,  3,  6, 10, 15]
         ↑   ↑   ↑   ↑   ↑   ↑
        起点  1   1+2 1+2+3 ...

prefix[i] = nums[0] + nums[1] + ... + nums[i]

前缀和:快速计算子数组和

通过前缀和,计算任意子数组的和就变成了一次减法

子数组 nums[i:j+1] 的和 = prefix[j] - prefix[i-1]

  • prefix[j] = nums[0] + nums[1] + ... + nums[j]
  • prefix[i-1] = nums[0] + nums[1] + ... + nums[i-1]
  • 两者一减,中间的元素就都消掉了,剩下 nums[i] + nums[i+1] + ... + nums[j]

一个简单例子:

复制代码
数组:     [1,  2,  3,  4,  5]
索引:      0   1   2   3   4

前缀和:
  prefix[0] = 1
  prefix[1] = 1 + 2 = 3
  prefix[2] = 1 + 2 + 3 = 6
  prefix[3] = 1 + 2 + 3 + 4 = 10
  prefix[4] = 1 + 2 + 3 + 4 + 5 = 15

现在要计算 nums[1:4](即索引 1, 2, 3 的元素:2, 3, 4)的和:

复制代码
直接用公式:prefix[3] - prefix[0] = 10 - 1 = 9 

使用前缀和缓存了之前计算的结果: 用一次减法秒算,不用重复累加。


回来看下原问题:有多少个子数组的和等于 k? 遍历数组,当前位置是 i,前 i 个数字的和是 x。 我们想找:有多少个之前的位置 j,使得从 j+1 到 i 的子数组和等于 k?

用前缀和的思路则求解过程编程:

复制代码
prefix[i] - prefix[j] = k
=> prefix[j] = prefix[i] - k
=> prefix[j] = x - k

所以问题转换成了:在 i 之前,有多少个位置的前缀和等于 (x - k)?

如何存储前缀和? 哈希表!

哈希表的作用

用一个哈希表来记录:

  • :前缀和的值
  • :这个前缀和出现过多少次
python 复制代码
def subarraySum_optimal(nums, k):
    # 哈希表:记录前缀和出现的次数
    prefix_count = {0: 1}  # 初始化:前缀和为0出现1次
    
    current_sum = 0
    result = 0
    
    for num in nums:
        current_sum += num
        
        # 关键问题:之前有多少个前缀和等于 (current_sum - k)?
        target = current_sum - k
        if target in prefix_count:
            result += prefix_count[target]
        
        # 更新哈希表:记录当前前缀和
        prefix_count[current_sum] = prefix_count.get(current_sum, 0) + 1
    
    return result

优化思路总结

阶段 做法 时间 问题
O(n³) 每次都 sum(nums[i:j+1]) 太慢 大量重复累加
O(n²) 用变量逐步累加 还是慢 仍然要对每个 (i,j) 对检查一遍
O(n) 前缀和 + 哈希表 反向查询,一遍扫描搞定

优化思路: 只扫描一遍数组,每个元素处理一次,哈希表的查询和插入都是 O(1)。


前缀和的进阶用法, 一个表格搞定

题目类型 关键变化 解法思路
和为 k 的子数组个数 求个数 哈希表记录前缀和出现次数
最长和为 k 的子数组 求长度 哈希表记录前缀和第一次出现的位置
最长 0 和 1 相等的子数组 把 0 看作 -1 问题转化为"和为 0 的子数组"
子数组和模 m 余 r 模运算 哈希表记录 prefix_sum % m

例子1:最长和为 k 的子数组

python 复制代码
def maxSubarrayLen(nums, k):
    prefix_sum = 0
    first_occurrence = {0: -1}  # 前缀和为0首次出现在位置-1
    max_len = 0
    
    for i, num in enumerate(nums):
        prefix_sum += num
        target = prefix_sum - k
        
        if target in first_occurrence:
            # 找到了一个和为k的子数组
            max_len = max(max_len, i - first_occurrence[target])
        
        # 只记录第一次出现(为了最大化长度)
        if prefix_sum not in first_occurrence:
            first_occurrence[prefix_sum] = i
    
    return max_len

例子2: 最大 XOR 子数组

类似的思路应用还有最大 XOR 子数组

原理是一样的,但实现不同

复制代码
前缀XOR: prefix_xor[i] = arr[0] ^ arr[1] ^ ... ^ arr[i]

子数组 [j+1, i] 的XOR值 = prefix_xor[i] ^ prefix_xor[j]

问题: 给定数组,求最大的子数组 XOR 值。

暴力想法: 枚举所有子数组,计算 XOR,O(n²)。

优化想法: 对每个 i,找到最优的 j 使得 prefix_xor[i] ^ prefix_xor[j] 最大。

这里不再用哈希表, 因为我们要最大化,不是查询具体值,引入新的数据结果 Trie 树按二进制位贪心匹配:

Trie树定义很简单:

python 复制代码
class TrieNode:
    def __init__(self):
        self.children = {}

def findMaximumXOR(nums):
    root = TrieNode()
    max_xor = 0
    prefix_xor = 0
    
    for num in nums:
        prefix_xor ^= num
        
        # 将前缀XOR的二进制表示插入Trie
        node = root
        for i in range(31, -1, -1):
            bit = (prefix_xor >> i) & 1
            if bit not in node.children:
                node.children[bit] = TrieNode()
            node = node.children[bit]
        
        # 贪心地找最大XOR:尽量走不同的分支
        node = root
        current_xor = 0
        for i in range(31, -1, -1):
            bit = (prefix_xor >> i) & 1
            # 优先选择相反的bit
            opposite_bit = 1 - bit
            if opposite_bit in node.children:
                current_xor |= (1 << i)
                node = node.children[opposite_bit]
            else:
                node = node.children[bit]
        
        max_xor = max(max_xor, current_xor)
    
    return max_xor

这个方法虽然看起来复杂,但思想是一致的:用数据结构快速查询之前的信息,避免暴力枚举


总结:类似问题的解题套路

题目共性:

  • 题目问"子数组"满足某个条件
  • 条件涉及"和"、"XOR"、"乘积"等可以累加的操作
  • 暴力解法是 O(n²) 或更差

整体的解题流程:

  1. 定义前缀信息(前缀和、前缀XOR等)
  2. 转化问题:从"找子数组"转化为"查询之前的前缀信息"
  3. 选择数据结构
    • 需要查询具体值?用哈希表
    • 需要查询最优值?用Trie树
  4. 一遍扫描:从左到右扫描数组,边更新边查询

最后

这个技巧整体还是比较好用的, 通过理解"前缀和 + 哈希表"的思路与技巧,其实很多看似不同的题目其实都是同一个套路的变种, 找到重复计算的地方, 用缓存消除重复计算

这样就有可能能从 O(n²) 的暴力简单思维中找到优化点,写出一个 O(n) 的优雅解法。

相关推荐
s***35302 小时前
Spring Boot3.x集成Flowable7.x(一)Spring Boot集成与设计、部署、发起、完成简单流程
java·spring boot·后端
rafael(一只小鱼)2 小时前
AI运维开发平台学习
java·开发语言
空空kkk2 小时前
SpringMVC——IO笔记
java·io
lcu1113 小时前
Java 学习40:继承
java
p***q783 小时前
【保姆级教程】apache-tomcat的安装配置教程
java·tomcat·apache
愤怒的山羊3 小时前
jetcache List 缓存, json 序列化 泛型解析成了 JsonObject 处理
缓存·json·list
2501_941148613 小时前
C++实时数据处理实战:多线程与异步IO结合高性能代码解析
java·后端·struts
u***u6853 小时前
C++在系统中的异常处理
java·开发语言·c++
树在风中摇曳3 小时前
带哨兵位的双向循环链表详解(含 C 代码)+ LeetCode138 深度解析 + 顺序表 vs 链表缓存机制对比(图解 CPU 层级)
c语言·链表·缓存