Java详解LeetCode 热题 100(10):LeetCode 560. 和为K的子数组(Subarray Sum Equals K)详解

1. 题目描述

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

示例 1:

输入:nums = [1,1,1], k = 2
输出:2
解释:此时有两个和为 2 的连续子数组:[1,1] 与 [1,1]

示例 2:

输入:nums = [1,2,3], k = 3
输出:2
解释:此时有两个和为 3 的连续子数组:[1,2] 与 [3]

提示:

  • 1 <= nums.length <= 2 * 10^4
  • -1000 <= nums[i] <= 1000
  • -10^7 <= k <= 10^7

2. 理解题目

这道题要求我们找出数组中所有和为k的连续子数组的数量。需要注意以下几点:

  1. 子数组必须是连续的,不能跳过元素
  2. 我们需要统计的是子数组的个数,而不是具体的子数组
  3. 数组元素可能包含负数,这意味着子数组的和可能增加也可能减少
  4. 同一位置的元素可以在不同的子数组中使用

解释一下示例:

  • 对于 nums = [1,1,1], k = 2:

    • 子数组 [1,1](索引 0 和 1)的和为 2
    • 子数组 [1,1](索引 1 和 2)的和为 2
    • 所以总共有 2 个和为 2 的子数组
  • 对于 nums = [1,2,3], k = 3:

    • 子数组 [1,2](索引 0 和 1)的和为 3
    • 子数组 [3](索引 2)的和为 3
    • 所以总共有 2 个和为 3 的子数组

3. 解法一:暴力枚举法

3.1 思路

最直观的方法是通过两层嵌套循环,枚举所有可能的子数组,然后计算它们的和。如果子数组的和等于 k,则计数器加 1。

具体步骤:

  1. 初始化计数器 count = 0
  2. 外层循环遍历所有可能的子数组起点
  3. 内层循环遍历所有可能的子数组终点
  4. 计算从起点到终点的子数组的和
  5. 如果和等于 k,则 count++
  6. 返回 count

3.2 Java代码实现

public class Solution {
   
    public int subarraySum(int[] nums, int k) {
   
        int count = 0;
        int n = nums.length;
        
        // 枚举所有可能的子数组
        for (int i = 0; i < n; i++) {
   
            int sum = 0;
            for (int j = i; j < n; j++) {
   
                // 计算子数组 nums[i...j] 的和
                sum += nums[j];
                
                // 如果子数组的和等于k,计数器加1
                if (sum == k) {
   
                    count++;
                }
            }
        }
        
        return count;
    }
}

3.3 代码详解

  1. 外层循环从索引 0 开始,遍历到 n-1,表示子数组的起始位置
  2. 内层循环从索引 i 开始,遍历到 n-1,表示子数组的结束位置
  3. 在内层循环中,我们累加 nums[j] 得到子数组 nums[i...j] 的和
  4. 如果累加和等于 k,则将计数器 count 加 1
  5. 最后返回计数器的值

3.4 复杂度分析

  • 时间复杂度:O(n²),其中 n 是数组长度。外层循环执行 n 次,内层循环平均执行 n/2 次,总体为 O(n²)。
  • 空间复杂度:O(1),只需要常数级的额外空间来存储计数器和求和变量。

3.5 适用场景

暴力法适用于数组长度较小的情况,但当数组长度较大时,O(n²) 的时间复杂度可能导致超时。

4. 解法二:前缀和解法

4.1 思路

暴力法的一个主要问题是重复计算了很多子数组的和。为了优化这一点,我们可以使用前缀和技术,这样可以在 O(1) 时间内得到任意子数组的和。

前缀和数组的定义为:preSum[i] 表示 nums[0] + nums[1] + ... + nums[i-1] 的和。
利用前缀和,我们可以在 O(1) 时间内计算子数组 nums[i...j] 的和:sum = preSum[j+1] - preSum[i]

具体步骤:

  1. 计算前缀和数组 preSum
  2. 使用两层循环枚举所有可能的子数组
  3. 使用前缀和公式计算子数组的和
  4. 如果和等于 k,则计数器加 1

4.2 Java代码实现

public class Solution {
   
    public int subarraySum(int[] nums, int k) {
   
        int count = 0;
        int n = nums.length;
        
        // 计算前缀和数组
        int[] preSum = new int[n + 1];
        for (int i = 0; i < n; i++) {
   
            preSum[i + 1] = preSum[i] + nums[i];
        }
        
        // 枚举所有可能的子数组
        for (int i = 0; i < n; i++) {
   
            for (int j = i; j < n; j++) {
   
                // 使用前缀和计算子数组 nums[i...j] 的和
                int sum = preSum[j + 1] - preSum[i];
                
                if (sum == k) {
   
                    count++;
                }
            }
        }
        
        return count;
    }
}

4.3 代码详解

  1. 创建一个长度为 n+1 的前缀和数组 preSum,其中 preSum[0] = 0
  2. 计算前缀和数组:preSum[i] = preSum[i-1] + nums[i-1]
  3. 使用两层循环枚举所有可能的子数组:
    • 外层循环表示子数组的起始位置
    • 内层循环表示子数组的结束位置
  4. 使用前缀和公式计算子数组的和:sum = preSum[j+1] - preSum[i]
  5. 如果 sum == k,则将计数器 count 加 1
  6. 返回最终的计数器值

4.4 复杂度分析

  • 时间复杂度:O(n²),其中 n 是数组长度。虽然使用了前缀和,但我们仍然需要两层循环来枚举所有可能的子数组。
  • 空间复杂度:O(n),需要额外的 O(n) 空间来存储前缀和数组。

4.5 适用场景

前缀和解法相比暴力法的优势不在于时间复杂度(两者都是 O(n²)),而在于通过预计算消除了重复计算子数组和的操作,使得内层循环的常数因子更小。

5. 解法三:前缀和 + 哈希表解法

5.1 思路

前面两种解法的时间复杂度都是 O(n²),对于较大的数组可能会超时。我们可以借助哈希表进一步优化,将时间复杂度降低到 O(n)。

关键思路:

  • 对于前缀和 preSum[j],如果存在某个前缀和 preSum[i] 使得 preSum[j] - preSum[i] = k,那么子数组 nums[i...j-1] 的和就等于 k
  • 我们可以转换思路:对于当前前缀和 curr_sum,我们需要查找之前是否出现过前缀和为 curr_sum - k 的位置
  • 使用哈希表记录每个前缀和出现的次数,可以在 O(1) 时间内完成查找

具体步骤:

  1. 创建一个哈希表 map,用于存储每个前缀和出现的次数
  2. 初始化 map[0] = 1(表示空子数组,前缀和为 0,出现 1 次)
  3. 初始化 curr_sum = 0(当前前缀和)和 count = 0(满足条件的子数组数量)
  4. 遍历数组 nums
    • 累加当前元素到 curr_sum
    • 判断 map 中是否存在键 curr_sum - k
    • 如果存在,则将其对应的值(出现次数)加到 count
    • 更新哈希表 map[curr_sum]++
  5. 返回 count

5.2 Java代码实现

import java.util.HashMap;
import java.util.Map;

public class Solution {
   
    public int subarraySum(int[] nums, int k) {
   
        // 哈希表:前缀和 -> 出现次数
        Map<Integer, Integer> prefixSumCount = new HashMap<>();
        
        // 初始化:空子数组的前缀和为0,出现次数为1
   
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

全栈凯哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值