LeetCode 热题 100——day10 和为 K 的子数组

✨ 把代码写进星轨,用逻辑丈量宇宙。

导航 链接
个人主页 🏠 星轨初途
基础语言专栏 💻 C语言📚 数据结构
刷题实战专栏 🚀 算法及编程题分享、📝 LeetCode 每日一题

LeetCode 560:和为 K 的子数组

LeetCode 560:和为 K 的子数组

🚀 每日一题继续!今天碰到的是一道前缀和经典题。

题目让我们统计"和恰好等于 k 的连续子数组个数"。看到连续区间求和,前缀和很容易想到;真正有意思的地方在于,怎样把枚举区间的 O(n²) 再压到 O(n)

这次哈希表又要登场啦 ( •̀ ω •́ )✧

文章目录

题目描述

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

示例一:

text 复制代码
输入:nums = [1,1,1], k = 2
输出:2

满足条件的两个子数组分别是:

text 复制代码
nums[0...1] = [1,1]
nums[1...2] = [1,1]

示例二:

text 复制代码
输入:nums = [1,2,3], k = 3
输出:2

对应的子数组是 [1,2][3]

从哪里开始想?

最直接的办法是枚举子数组的左右端点,再计算这段区间的和。

如果每次都重新累加区间元素,时间复杂度会达到 O(n³)。这个做法显然有点笨,因为大量区间存在重叠,同一批数字会被反复计算。

前缀和可以解决"重复求区间和"的问题。

设:

text 复制代码
pre[i] 表示 nums[0...i-1] 的元素之和

于是区间 [left, right] 的和为:

text 复制代码
pre[right + 1] - pre[left]

查询一次区间和只需要 O(1),总复杂度便从 O(n³) 降到了 O(n²)

方法一:前缀和 + 枚举区间

先求出前缀和数组,再枚举子数组的左右端点。

cpp 复制代码
class Solution
{
public:
    int subarraySum(vector<int>& nums, int k)
    {
        int n = nums.size();
        vector<int> pre(n + 1, 0);

        for (int i = 0; i < n; ++i)
        {
            pre[i + 1] = pre[i] + nums[i];
        }

        int ans = 0;
        for (int left = 0; left < n; ++left)
        {
            for (int right = left; right < n; ++right)
            {
                if (pre[right + 1] - pre[left] == k)
                {
                    ++ans;
                }
            }
        }

        return ans;
    }
};

复杂度分析

  • 时间复杂度:O(n²)
  • 空间复杂度:O(n)

前缀和已经帮我们快速算出了区间和,但左右端点仍然需要两层循环。还能不能继续优化?

可以。把公式稍微变一下,左端点就不用枚举了。

方法二:前缀和 + 哈希表

假设当前遍历到位置 i,当前前缀和为 pre[i]。如果前面存在一个前缀和 pre[j],满足:

text 复制代码
pre[i] - pre[j] = k

移项可得:

text 复制代码
pre[j] = pre[i] - k

问题一下就变了:

遍历到当前前缀和时,前面出现过多少次 pre[i] - k

我们只需要用哈希表记录每个前缀和出现的次数。每得到一个新的前缀和 sum,就去查找 sum - k

text 复制代码
找到一次,说明有一个子数组的和为 k;
找到多次,说明有多个不同起点都能组成答案。

因此哈希表保存的不是"某个前缀和是否出现",而是"它出现了几次"。

用示例走一遍

对于:

text 复制代码
nums = [1,1,1], k = 2

初始化:

text 复制代码
mp[0] = 1

遍历过程如下:

当前数字 当前前缀和 sum 查找 sum-k 出现次数 答案 cnt
1 1 -1 0 0
1 2 0 1 1
1 3 1 1 2

最终答案为 2

为什么要先写 mp[0] = 1

0 表示还没有取任何元素时的前缀和。

如果当前前缀和本身就等于 k

text 复制代码
sum - k = 0

这说明从数组下标 0 开始到当前位置的子数组符合要求。提前放入一次前缀和 0,就能把这种情况自然地统计进去。

例如:

text 复制代码
nums = [1,2], k = 3

遍历到 2 时,当前前缀和为 3。我们查找 3 - 3 = 0,正好在哈希表中找到一次,于是 [1,2] 被计入答案。

代码实现

下面是本次打卡使用的写法,使用 brr 保存所有前缀和:

cpp 复制代码
class Solution
{
public:
    int subarraySum(vector<int>& nums, int k)
    {
        vector<int> brr(nums.size() + 1, 0);
        unordered_map<int, int> mp;

        // 空前缀出现一次,用来统计从下标 0 开始的合法子数组
        mp[0] = 1;

        int cnt = 0;
        for (int i = 0; i < nums.size(); ++i)
        {
            // 计算当前位置的前缀和
            brr[i + 1] = brr[i] + nums[i];

            // 如果前面出现过 brr[i + 1] - k,
            // 那么这些位置到当前下标都能组成和为 k 的子数组
            if (mp.find(brr[i + 1] - k) != mp.end())
            {
                cnt += mp[brr[i + 1] - k];
            }

            // 当前前缀和加入哈希表,供后面的元素使用
            mp[brr[i + 1]]++;
        }

        return cnt;
    }
};

为什么要先查找,再记录当前前缀和?

子数组必须非空。

k = 0 时,如果先执行:

cpp 复制代码
mp[sum]++;

再查找 sum - k,当前前缀和会把自己也统计进去,相当于计算了一个长度为 0 的空区间。

所以顺序不能换:

text 复制代码
先寻找以前的前缀和
再记录当前前缀和

还能再简化一点

哈希表只会查询以前出现过的前缀和,并不需要通过下标访问整个 brr 数组。用一个变量 sum 维护当前前缀和就够了:

cpp 复制代码
class Solution
{
public:
    int subarraySum(vector<int>& nums, int k)
    {
        unordered_map<int, int> mp;
        mp[0] = 1;

        int sum = 0;
        int ans = 0;

        for (int num : nums)
        {
            sum += num;

            if (mp.find(sum - k) != mp.end())
            {
                ans += mp[sum - k];
            }

            mp[sum]++;
        }

        return ans;
    }
};

这两份代码的时间、空间复杂度相同。第二份只是省掉了前缀和数组,常数空间更小,写起来也更利落。

复杂度分析

  • 时间复杂度:平均 O(n),每个元素只遍历一次,哈希表查询和插入平均为 O(1)
  • 空间复杂度:O(n),最坏情况下每个前缀和都不同。

为什么这道题不适合滑动窗口?

题目中的 nums[i] 可能是负数。

如果数组全是正数,窗口扩大后区间和只会增加,缩小后只会减少,我们可以根据和的大小移动左右指针。但负数加入窗口后,区间和可能变大,也可能变小,滑动窗口失去了明确的移动方向。

例如:

text 复制代码
nums = [2,-1,2], k = 3

加入 -1 后窗口和反而减小,仅根据当前和与 k 的关系,无法判断应该移动哪一侧。

前缀和配合哈希表不依赖单调性,因此可以正常处理正数、负数和 0

两种方法对比

方法 做法 时间复杂度 空间复杂度
前缀和 + 枚举区间 枚举左右端点,O(1) 查询区间和 O(n²) O(n)
前缀和 + 哈希表 统计 sum-k 以前出现的次数 O(n) O(n)

总结 (๑•̀ㅂ•́)و✧

这道题的突破口藏在前缀和公式里:

text 复制代码
pre[i] - pre[j] = k

把它变形成:

text 复制代码
pre[j] = pre[i] - k

原本需要枚举的左端点,就变成了哈希表里一次平均 O(1) 的查询。

还有两个细节别忘啦:mp[0] = 1 负责接住从下标 0 开始的子数组,查询必须放在记录当前前缀和之前,避免把空区间算进答案。

🎉 前缀和与哈希表成功会师!以后再遇到"连续子数组 + 区间和 + 统计个数",就可以往这个方向想一想。每日一题继续出发,下一题见!

相关推荐
数模竞赛Paid answer1 小时前
2021年深圳杯数学建模C题配电网可靠性和故障软自愈研究解题全过程论文及程序
算法·数学建模·数据分析·深圳杯
sycmancia1 小时前
Qt——自定义控件温度计
开发语言·qt
有点。1 小时前
C++广度优先搜索(二)-练习题
c++·算法·宽度优先
Q741_1471 小时前
TcpDump 使用笔记
网络·c++·笔记·测试工具·tcpdump
程序猿编码1 小时前
不用PyTorch,不用CUDA,我用C++手写了一个能跑GPT和Whisper的推理库
c++·pytorch·gpt·大模型·whisper
疯狂打码的少年1 小时前
【数据结构】队列的应用:循环队列
数据结构·笔记·算法
evans在进步1 小时前
LeetCode 17:电话号码的字母组合——Java DFS 回溯法详解
java·leetcode·深度优先
MC皮蛋侠客1 小时前
TDengine C++ 系列(1):全景与最小闭环——从时序数据到第一个 C++ 读写
大数据·c++·tdengine
UQR2 小时前
一、Java基础高频面试题
java·开发语言