📌 题目链接:(leetcode.cn/problems/su…)
🔍 难度:中等 | 🏷️ 标签:数组、前缀和、哈希表
⏱️ 目标时间复杂度:O(n)
💾 空间复杂度:O(n)
题目链接
题目分析
给你一个整数数组
nums和一个整数k,请你统计并返回 该数组中和为 k 的连续非空子数组的个数。
- 子数组必须是连续的,不能跳跃;
- 元素可正可负,因此不能使用滑动窗口(因为无法判断何时收缩);
- 举例:
nums = [1,1,1], k = 2→ 有两个子数组[1,1](位置0-1 和 1-2)→ 输出2nums = [1,2,3], k = 3→ 子数组[1,2]和[3]→ 输出2
⚠️ 注意:由于存在负数,前缀和不具备单调性,所以不能用双指针或滑动窗口!
核心算法及代码讲解
✅ 核心思想:前缀和 + 哈希表优化
什么是前缀和?
定义 pre[i] 表示从 nums[0] 到 nums[i] 的累加和。
那么任意子数组 [j, i] 的和为:
我们希望:
👉 因此,对于当前前缀和 pre[i],只需知道有多少个之前的前缀和等于 pre[i] - k,就能得到以 i 结尾的满足条件的子数组个数。
为什么用哈希表?
- 我们需要快速查询“之前出现过多少次某个前缀和”;
- 哈希表
unordered_map<int, int>:键为前缀和值,值为该前缀和出现的次数; - 初始化:
mp[0] = 1,表示前缀和为0出现1次(对应空数组,用于处理从开头开始的子数组);
关键细节
- 先查后更新:必须在更新当前
pre到哈希表之前查询pre - k,否则会把当前自己也算进去(导致错误计数); - 负数和零都合法,哈希表能正确处理;
- 时间复杂度从暴力 O(n²) 优化到 O(n),空间换时间的经典案例。
代码与行注释(与解题思路完全一致)
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
unordered_map<int, int> mp; // 哈希表:记录前缀和及其出现次数
mp[0] = 1; // 初始化:前缀和为0出现1次(空子数组)
int count = 0; // 结果计数器
int pre = 0; // 当前前缀和(滚动变量,节省空间)
for (auto& x : nums) { // 遍历每个元素
pre += x; // 更新当前前缀和
// 查找是否存在前缀和 = pre - k
if (mp.find(pre - k) != mp.end()) {
count += mp[pre - k]; // 累加符合条件的子数组个数
}
mp[pre]++; // 将当前前缀和加入哈希表(注意:在查询之后!)
}
return count;
}
};
解题思路
分步骤拆解算法逻辑:
- 理解问题本质:求连续子数组和为
k的个数 → 转化为前缀和差值问题; - 暴力法局限:双重循环 O(n²),对大数据(n=2×10⁴)勉强通过但不够优雅;
- 引入前缀和:将子数组和转化为两个前缀和之差;
- 数学转化:
pre[i] - pre[j] = k⇒pre[j] = pre[i] - k; - 哈希表加速:用 map 记录历史前缀和出现频次,实现 O(1) 查询;
- 边界处理:初始化
mp[0]=1,确保从索引0开始的子数组能被正确计算; - 遍历顺序:边遍历、边查询、边更新,保证只使用左侧信息(避免未来污染)。
算法分析
| 项目 | 分析 |
|---|---|
| 时间复杂度 | O(n):仅需一次遍历,每次哈希操作 O(1) |
| 空间复杂度 | O(n):最坏情况下所有前缀和都不同,哈希表存 n 个键值对 |
| 适用场景 | 数组中有正有负、要求子数组和为定值的问题 |
| 面试高频点 | ✅ 前缀和思想 ✅ 哈希表优化技巧 ✅ 边界初始化(mp[0]=1) ✅ “先查后插”顺序的重要性 |
| 常见变体 | - 和为0的子数组 - 和能被k整除的子数组 - 最长和为k的子数组(需记录首次出现位置) |
💡 面试官可能追问:
- 如果数组全是正数,能否用滑动窗口?(可以,因为前缀和单调递增)
- 如果要求返回所有子数组的起止索引怎么办?(需记录每个前缀和首次/所有出现位置)
- 如何处理 k 很大或溢出的情况?(用 long long 防溢出)
代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
unordered_map<int, int> mp;
mp[0] = 1;
int count = 0, pre = 0;
for (auto& x:nums) {
pre += x;
if (mp.find(pre - k) != mp.end()) {
count += mp[pre - k];
}
mp[pre]++;
}
return count;
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
Solution sol;
// 测试用例1
vector<int> nums1 = {1, 1, 1};
int k1 = 2;
cout << "Test 1: " << sol.subarraySum(nums1, k1) << " (Expected: 2)" << endl;
// 测试用例2
vector<int> nums2 = {1, 2, 3};
int k2 = 3;
cout << "Test 2: " << sol.subarraySum(nums2, k2) << " (Expected: 2)" << endl;
// 测试用例3:包含负数
vector<int> nums3 = {1, -1, 0};
int k3 = 0;
cout << "Test 3: " << sol.subarraySum(nums3, k3) << " (Expected: 3)" << endl;
// 测试用例4:单个元素
vector<int> nums4 = {1};
int k4 = 1;
cout << "Test 4: " << sol.subarraySum(nums4, k4) << " (Expected: 1)" << endl;
return 0;
}
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📣 下一期预告:LeetCode 热题 100 第11题 —— 滑动窗口最大值(困难)
🔹 题目:给你一个整数数组
nums,有一个大小为k的滑动窗口从数组的最左侧移动到最右侧。你只能看到在滑动窗口内的k个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值。🔹 核心思路:使用 单调队列(双端队列)维护窗口内最大值,确保队首始终是当前窗口的最大值。
🔹 考点:滑动窗口、单调队列、双端队列、动态维护极值。
🔹 难度:困难,是数据结构与算法结合的经典题,常考于大厂面试(如腾讯、字节、阿里)。
💡 提示:不要暴力枚举每个窗口!要用“先进先出 + 单调性”来优化。队列中只保留可能成为最大值的候选者!
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!