ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

algorithm-base 仓库题解:LeetCode 560 和为 K 的子数组——从暴力枚举到前缀和 + HashMap 的完整推导

algorithm-base 仓库题解:LeetCode 560 和为 K 的子数组——从暴力枚举到前缀和 + HashMap 的完整推导 文档教程知识库【免费下载链接】algorithm-base一位酷爱做饭的程序员立志用动画将算法说的通俗易懂。我的面试网站 www.chengxuchu.com项目地址https://gitcode.com/gh_mirrors/al/algorithm-base点击查看免费下载导读本文讲解 LeetCode 560「和为 K 的子数组」在 algorithm-base 仓库中的完整题解脉络先给出最容易理解的双重循环暴力法再引入前缀和数组将子数组求和降为常数时间最后推导出前缀和 HashMap的 O(n) 最优解法并重点剖析为什么要预先放入map.put(0, 1)这一经典细节。读完本文你不仅能 AC 本题还能掌握连续子数组区间和这一类问题的通用分析框架为仓库中前缀和系列的后续题目如 974、523、1248打好基础。题目描述与示例给定一个整数数组nums和一个整数k你需要找到该数组中和为k的连续的子数组的个数。示例 1输入nums [1,1,1]k 2输出2其中[1,1]与[1,1]为两种不同的情况。注意题意中的两个关键词连续只统计连续的子数组不要求子数组不重叠个数相同的子数组区间出现多次时要分别计数。因此示例中两个[1,1]虽然数值相同但分别对应下标区间[0,1]和[1,2]需要统计两次。本题在仓库中的原始题解位于 animation-simulation/数组篇/leetcode560和为K的子数组.md与 animation-simulation/前缀和/leetcode560和为K的子数组.md 内容互为补充后者还给出了 C 的map写法。解法一暴力双重循环思路解析题意直白最容易想到的就是固定左端点i然后用内层循环从i开始累加右端点j一旦累加和等于k就计数。两层循环枚举了所有i ≤ j的连续子数组因此不会漏解。Java 代码class Solution { public int subarraySum(int[] nums, int k) { int len nums.length; int sum 0; int count 0; for (int i 0; i len; i) { for (int j i; j len; j) { sum nums[j]; if (sum k) { count; } } sum 0; // 重置累加和开始下一个左端点 } return count; } }代码中有两个容易忽视的细节sum 0必须放在内层循环结束后执行保证每个左端点i都从 0 开始累加内层循环从j i开始保证子数组至少包含一个元素非空。复杂度分析时间复杂度O(n²)两层循环枚举全部子区间空间复杂度O(1)只用了常数个变量。原文档明确指出Python3 版本与 Swift 版本的暴力代码在 LeetCode 上会超时这印证了 O(n²) 无法通过本题的数据规模必须继续优化。解法二前缀和数组presum什么是前缀和前缀和这个思想其实我们很早就接触过求数列的和时Sn a1 a2 ... an就是数列的前 n 项和。比如S5 a1a2a3a4a5S2 a1a2那么S5 - S2 a3a4a5即通过两段前缀和之差直接得到某个连续区间的和——这正是前缀和思想的核心。在本题场景中我们构造前缀和数组presum其中presum[i]保存nums前 i 项的和递推关系为presum[1] presum[0] nums[0]presum[2] presum[1] nums[1]presum[3] presum[2] nums[2]……由此任意区间nums[i..j]的和都可以用两次前缀和相减得到sum(i, j) presum[j1] - presum[i]。从源码结构看这种用预处理数组保存历史信息、再通过相减/比对快速回答区间问题的思路与仓库数据结构和算法目录下 KMP.md、BM.md 中利用 next 数组、suffix 数组加速匹配的思想有异曲同工之处——都是以空间换时间用预处理结果避免重复计算。区间和的推导以获取nums[2]到nums[4]这个区间的和为例前 5 项的和为presum[5]前 2 项的和为presum[2]区间和 presum[5] - presum[2]。也就是前 5 项的和减去前 2 项的和得到第 3 项到第 5 项的和。Java 代码class Solution { public int subarraySum(int[] nums, int k) { // 前缀和数组 int[] presum new int[nums.length 1]; for (int i 0; i nums.length; i) { // 这里需要注意我们的前缀和是 presum[1] 开始填充的 presum[i 1] nums[i] presum[i]; } // 统计个数 int count 0; for (int i 0; i nums.length; i) { for (int j i; j nums.length; j) { // 注意偏移因为 nums[2] 到 nums[4] 等于 presum[5] - presum[2] // 所以这样就可以得到 nums[i, j] 区间内的和 if (presum[j 1] - presum[i] k) { count; } } } return count; } }这段代码有三个值得留意的点数组长度是nums.length 1presum[0] 0表示空前缀的和presum[i1]才对应前 i1 项的实际和这样presum[j1] - presum[i]恰好覆盖nums[i..j]索引偏移清晰不易出错复杂度时间仍为 O(n²)枚举所有区间空间升为 O(n)额外的前缀和数组仍然超时原文档同样注明 Python3 版本会超时——前缀和只是把求和从 O(n) 降为 O(1)但枚举区间本身仍是 O(n²)。所以真正需要解决的是如何不枚举区间也能统计出满足presum[j1] - presum[i] k的(i, j)对数。解法三前缀和 HashMapO(n) 最优解核心推导上面的分析已经把问题转化为对于每个右端点j有多少个左端点i满足presum[j1] - presum[i] k移项得presum[i] presum[j1] - k也就是说当我们遍历到某个前缀和presum[j1]时只需要知道在此之前出现过多少次presum[j1] - k这个前缀和值就立即得到了以j为右端点的合法子数组个数。由于我们只需要某个前缀和值出现的次数且遍历过程中要求能快速查询与更新自然会想到哈希表key 是前缀和的值value 是该前缀和出现的次数。这一步与仓库 求和问题/两数之和.md 中 HashMap 解法的思想完全一致两数之和把target - x是否存在于哈希表中作为判断依据本题则把presum - k是否出现过作为判断依据。区别只在于两数之和的 map 以数组值为 key、索引为 value而本题的 map 以前缀和值为 key、出现次数为 value。两数之和 HashMap 解法回顾class Solution { public int[] twoSum(int[] nums, int target) { HashMapInteger, Integer map new HashMap(); // 一次遍历 for (int i 0; i nums.length; i) { // 存在时我们用数组的值为 key索引为 value if (map.containsKey(target - nums[i])) { return new int[]{i, map.get(target - nums[i])}; } // 存入值 map.put(nums[i], i); } // 返回 return new int[]{}; } }关键细节为什么要map.put(0, 1)这是本题最容易漏掉的细节也是面试高频考点。假设数组为[1, 1, 0]k 2遍历到第 3 位时前缀和presum 2此时需要查找presum - k 0是否出现过但0这个前缀和空前缀在遍历中从未被存入 map若没有预先放入map.put(0, 1)就会漏掉[1,1]对应前缀和 2 - 前缀和 0和[1,1,0]对应前缀和 2 - 前缀和 0右侧补 0这两个合法答案最终错误地返回 0。原文档给出的对照实验输入[3,1,1,0]k 2时即便漏掉map.put(0, 1)也不出错——因为当presum 2时 map 中已有此前存入的2由31产生… 但[1,1,0]这种前几位刚好凑成 k的情况必须靠map.put(0, 1)垫底才能正确统计。因此可以总结为一条规律当区间和公式presum[j1] - presum[i] k中的presum[i]可能取 0即区间从数组开头开始时必须预先放入map.put(0, 1)。Java 代码最终版class Solution { public int subarraySum(int[] nums, int k) { if (nums.length 0) { return 0; } HashMapInteger, Integer map new HashMap(); // 细节这里需要预存前缀和为 0 的情况否则会漏掉前几位就满足的情况 // 例如输入 [1,1,0]k 2如果没有这行代码则会返回 0 // 漏掉了 112 和 1102 的情况 // 输入 [3,1,1,0]k 2 时则不会漏掉 // 因为 presum[3] - presum[0] 表示前面 3 位的和所以需要 map.put(0,1) 垫下底 map.put(0, 1); int count 0; int presum 0; for (int x : nums) { presum x; // 当前前缀和已知判断是否含有 presum - k 的前缀和 // 存在则说明某一区间的和为 k 了 if (map.containsKey(presum - k)) { count map.get(presum - k); // 获取 presum - k 前缀和出现次数 } // 更新当前前缀和出现次数 map.put(presum, map.getOrDefault(presum, 0) 1); } return count; } }复杂度分析时间复杂度O(n)单次线性遍历每次哈希表查询与插入均为 O(1) 摊还空间复杂度O(n)哈希表最多保存 n1 个不同的前缀和值。分语言实现原文档还给出了 Swift、C、Go 三种语言的实现逻辑与 Java 完全一致可直接对照阅读。Swift Codeclass Solution { func subarraySum(_ nums: [Int], _ k: Int) - Int { if nums.count 0 { return 0 } var map: [Int: Int] [:] map[0] 1 // 需要添加入一个元素垫底以支持前几位就满足的情况 var presum 0, count 0 for x in nums { presum x // 当前前缀和已知判断是否含有 presum - k 的前缀和 if let v map[presum - k] { count v // 获取 presum - k 前缀和出现次数 } map[presum] (map[presum] ?? 0) 1 } return count } }C Code数组篇文档版本unordered_mapclass Solution { public: int subarraySum(vectorint nums, int k) { unordered_mapint, int smp; int sum 0; // 初始化最外面的 0 smp[0] 1; int result 0; for (int i 0; i nums.size(); i) { sum nums[i]; auto mp smp.find(sum - k); if (mp ! smp.end()) { // map 里面存的一定是在前面的元素 // 可以尝试将 map 的 value 换为数组 result mp-second; } smp[sum]; } return result; } };Go Codefunc subarraySum(nums []int, k int) int { m : map[int]int{} m[0] 1 sum : 0 cnt : 0 for _, num : range nums { sum num if v, ok : m[sum-k]; ok { cnt v } m[sum] } return cnt }从这道题建立前缀和解题框架理解本题后可以将其抽象为通用模型这也是仓库前缀和系列的组织思路目标推导哈希表 key典型题目仓库路径和为 k 的区间个数presum[j1] - presum[i] k前缀和值本题 560数组篇 / 前缀和篇和可被 k 整除的区间个数(presum[j1] - presum[i]) % k 0即presum[j1] % k presum[i] % k前缀和对 k 的余数负数需纠正为(x % k k) % kleetcode974 和可被 K 整除的子数组存在长度 ≥ 2 且和为 k 倍数的子数组同余 索引距离判断前缀和对 k 的余数value 存最小索引且map.put(0, -1)垫底leetcode523 连续的子数组和中心索引左侧和 右侧和等价于比较两个前缀和无需哈希表一次遍历 一次比较leetcode724 寻找数组的中心索引几个可以直接复用的结论凡是要快速回答连续子数组区间和问题优先考虑前缀和需要统计个数时哈希表 value 存出现次数本题需要判断长度时value 存最小索引523涉及取模运算时注意负数取模的纠正key (presum % K K) % K可参见 leetcode974 中[-1,2,9]、K2的例子边界条件用空前缀垫底map.put(0, 1)统计个数或map.put(0, -1)计算长度避免漏掉从数组头部开始的子数组。延伸阅读前缀和与区间和的基础推导前缀和详解724 中心索引同余思路的姊妹题leetcode974 和可被 K 整除的子数组加长度 ≥ 2限制的升级题leetcode523 连续的子数组和前缀和与计数结合的滑动窗口变体leetcode1248 寻找优美子数组与本题共用 HashMap 技巧的两数之和求和问题/两数之和.md仓库中对数组区间类题目的其他练习数组篇 leetcode1 两数之和、数组篇 leetcode560 和为 K 的子数组赞分享文档教程知识库【免费下载链接】algorithm-base一位酷爱做饭的程序员立志用动画将算法说的通俗易懂。我的面试网站 www.chengxuchu.com项目地址https://gitcode.com/gh_mirrors/al/algorithm-base点击查看免费下载相关推荐LeetCode 560 和为 K 的子数组题解前缀和 哈希表 O(n) 解法详解LeetCode 560 和为 K 的子数组题解前缀和 哈希表 O n 解法详解 本文基于本仓库 problems/560.subarray sum eq文档教程知识库LeetCode 560和为 K 的子数组Subarray Sum Equals K——前缀和 哈希表 O(n) 解法全解析LeetCode 560和为 K 的子数组Subarray Sum Equals K——前缀和 哈希表 O n 解法全解析 本文基于 leetcode文档教程知识库LeetCode 560 Subarray Sum Equals K 题解前缀和 哈希表统计子数组和等于 k 的个数LeetCode 560 Subarray Sum Equals K 题解前缀和 哈希表统计子数组和等于 k 的个数 本篇技术指南围绕 LeetCode示例工程教程上一篇茉莉花Zotero插件如何快速解决中文文献管理的三大痛点下一篇GTA5线上小助手免费开源的终极游戏增强工具彻底改变你的洛圣都体验创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表