【LeetCode Hot100 刷题日记 (10/100)】560. 和为 K 的子数组 —— 数组、前缀和、哈希表 🧮

69 阅读5分钟

📌 题目链接:(leetcode.cn/problems/su…)

🔍 难度:中等 | 🏷️ 标签:数组、前缀和、哈希表

⏱️ 目标时间复杂度:O(n)

💾 空间复杂度:O(n)


题目链接

560. 和为 K 的子数组 - 力扣(LeetCode)

题目分析

给你一个整数数组 nums 和一个整数 k,请你统计并返回 该数组中和为 k 的连续非空子数组的个数

  • 子数组必须是连续的,不能跳跃;
  • 元素可正可负,因此不能使用滑动窗口(因为无法判断何时收缩);
  • 举例:
    • nums = [1,1,1], k = 2 → 有两个子数组 [1,1](位置0-1 和 1-2)→ 输出 2
    • nums = [1,2,3], k = 3 → 子数组 [1,2][3] → 输出 2

⚠️ 注意:由于存在负数,前缀和不具备单调性,所以不能用双指针或滑动窗口!


核心算法及代码讲解

核心思想:前缀和 + 哈希表优化

什么是前缀和?

定义 pre[i] 表示从 nums[0]nums[i] 的累加和。
那么任意子数组 [j, i] 的和为:

sum(j,i)=pre[i]pre[j1]\text{sum}(j, i) = pre[i] - pre[j-1]

我们希望:

pre[i]pre[j1]=kpre[j1]=pre[i]kpre[i] - pre[j-1] = k \quad \Rightarrow \quad pre[j-1] = pre[i] - k

👉 因此,对于当前前缀和 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;
    }
};

解题思路

分步骤拆解算法逻辑:

  1. 理解问题本质:求连续子数组和为 k 的个数 → 转化为前缀和差值问题;
  2. 暴力法局限:双重循环 O(n²),对大数据(n=2×10⁴)勉强通过但不够优雅;
  3. 引入前缀和:将子数组和转化为两个前缀和之差;
  4. 数学转化pre[i] - pre[j] = kpre[j] = pre[i] - k
  5. 哈希表加速:用 map 记录历史前缀和出现频次,实现 O(1) 查询;
  6. 边界处理:初始化 mp[0]=1,确保从索引0开始的子数组能被正确计算;
  7. 遍历顺序:边遍历、边查询、边更新,保证只使用左侧信息(避免未来污染)。

算法分析

项目分析
时间复杂度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 个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值。

🔹 核心思路:使用 单调队列(双端队列)维护窗口内最大值,确保队首始终是当前窗口的最大值。

🔹 考点:滑动窗口、单调队列、双端队列、动态维护极值。

🔹 难度:困难,是数据结构与算法结合的经典题,常考于大厂面试(如腾讯、字节、阿里)。

💡 提示:不要暴力枚举每个窗口!要用“先进先出 + 单调性”来优化。队列中只保留可能成为最大值的候选者!


📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!