力扣解题-560. 和为 K 的子数组

6 阅读6分钟

力扣解题-560. 和为 K 的子数组

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

子数组是数组中元素的连续非空序列。

示例 1: 输入:nums = [1,1,1], k = 2 输出:2

示例 2: 输入:nums = [1,2,3], k = 3 输出:2

提示:

1 <= nums.length <= 2 * 104

-1000 <= nums[i] <= 1000

-107 <= k <= 107


第一次解答

解题思路

核心方法:前缀和 + 哈希表统计频次,将暴力枚举子数组的O(n²)时间复杂度优化至O(n),利用前缀和的数学性质快速定位和为k的子数组,是该问题的最优解法。

核心原理铺垫

前缀和定义:prefixSum[i] 表示数组前i个元素的和(prefixSum[0] = 0prefixSum[1] = nums[0]prefixSum[2] = nums[0]+nums[1],以此类推)。 子数组和公式:对于子数组nums[start...end](从下标startend),其和 = prefixSum[end+1] - prefixSum[start]。 问题转化:要找和为k的子数组,即满足 prefixSum[end+1] - prefixSum[start] = kprefixSum[start] = prefixSum[end+1] - k。 因此,遍历到end位置时,只需统计之前出现过的prefixSum[start] = (当前前缀和 - k)的次数,即可得到以end为结尾的、和为k的子数组个数。

具体步骤
  1. 初始化关键变量
    • prefixCountHashMap<Long, Integer>,键为前缀和,值为该前缀和出现的次数(解决重复前缀和的统计问题);
    • 初始化prefixCount.put(0L, 1):表示“前缀和为0”的情况出现1次(对应“空前缀”,即数组开始之前的虚拟前缀,用于处理从下标0开始的子数组);
    • currSumlong类型的当前前缀和(避免int溢出,因为nums元素可正可负,累加后可能超出int范围),初始值为0;
    • count:统计和为k的子数组总数,初始值为0。
  2. 遍历数组计算前缀和
    • 遍历每个元素num,将num累加到currSum,得到当前前缀和;
    • 计算目标前缀和target = currSum - k:若存在该前缀和,说明有子数组以当前位置结尾且和为k;
    • prefixCount包含target,则将count加上prefixCount.get(target)(即符合条件的子数组个数);
    • 将当前前缀和currSum存入prefixCount:若已存在则次数+1,不存在则初始化为1(prefixCount.getOrDefault(currSum, 0)+1)。
  3. 返回结果:遍历完成后,count即为和为k的子数组总数,直接返回。
核心优化逻辑说明
  1. 时间复杂度优化:暴力解法需枚举所有子数组(O(n²)),无法适配n=2×10⁴的规模;前缀和+哈希表仅需一次遍历(O(n)),哈希表的查找/插入均为O(1),整体时间复杂度为O(n),完全满足题目性能要求。
  2. 数据类型选择:使用long存储前缀和,避免因nums元素累加(如2×10⁴个1000相加)导致int溢出,保证计算准确性。
  3. 性能表现说明:
    • 耗时30ms击败25.84%的用户:该解法是本题的标准最优解,多数提交均采用此逻辑,耗时差异主要来自评测机环境(如哈希表的哈希冲突、JVM优化等);
    • 内存消耗48.3MB击败23.38%的用户:核心原因是哈希表存储了所有前缀和的频次,空间复杂度为O(n)(最坏情况下所有前缀和唯一),属于合理的空间开销。

执行耗时:30 ms,击败了25.84% 的Java用户 内存消耗:48.3 MB,击败了23.38% 的Java用户

public int subarraySum(int[] nums, int k) {
        Map<Long,Integer> prefixCount=new HashMap<>();
        prefixCount.put(0L,1);
        long currSum=0;
        int count=0;
        for(int num:nums){
            currSum=currSum+num;
            long target=currSum-k;
            if(prefixCount.containsKey(target)){
                count=count+prefixCount.get(target);
            }
            prefixCount.put(currSum,prefixCount.getOrDefault(currSum,0)+1);
        }
        return count;
    }

扩展1:输出符合条件的子数组

扩展原理

在基础解法的哈希表中,不再仅存储前缀和的出现次数,而是存储该前缀和对应的所有索引(即前缀和结束的位置)。遍历过程中,找到目标前缀和后,通过索引还原出具体的子数组元素。

具体逻辑
  1. prefixMapMap<Long, List<Integer>>,键为前缀和,值为该前缀和出现的所有下标(如前缀和0对应下标-1,前缀和1对应下标0等);
  2. 初始化prefixMap.put(0L, new ArrayList<>(Arrays.asList(-1))):空前缀的下标为-1,用于还原从0开始的子数组;
  3. 遍历到下标i时,若找到目标前缀和target,则遍历prefixMap.get(target)中的所有起始前缀下标startIndex
    • 子数组的实际起始下标为startIndex + 1(因为startIndex是前缀和的结束下标,子数组从下一个位置开始);
    • 子数组的结束下标为i,遍历[startIndex+1, i]的元素,封装为列表加入结果;
  4. 每次遍历后,将当前下标i加入prefixMap中对应前缀和的列表,记录该前缀和的出现位置。
public List<List<Integer>> subarraysWithSumK(int[] nums, int k) {
    List<List<Integer>> result = new ArrayList<>();
    
    // Map<前缀和, 所有对应的前缀索引(即前缀和结束的位置)>
    Map<Long, List<Integer>> prefixMap = new HashMap<>();
    
    // 初始化:空前缀,前缀和=0,对应索引=-1(表示在数组开始之前)
    prefixMap.put(0L, new ArrayList<>(Arrays.asList(-1)));
    
    long currSum = 0;
    
    for (int i = 0; i < nums.length; i++) {
        currSum += nums[i];
        long target = currSum - k;
        
        // 如果存在 target,说明有子数组以 i 结尾且和为 k
        if (prefixMap.containsKey(target)) {
            for (int startIndex : prefixMap.get(target)) {
                // 子数组从 startIndex + 1 到 i
                List<Integer> subarray = new ArrayList<>();
                for (int j = startIndex + 1; j <= i; j++) {
                    subarray.add(nums[j]);
                }
                result.add(subarray);
            }
        }
        
        // 将当前前缀和对应的索引 i 加入 map
        prefixMap.computeIfAbsent(currSum, key -> new ArrayList<>()).add(i);
    }
    
    return result;
}

扩展2:输出符合条件的子数组的索引

扩展原理

与“输出子数组元素”的逻辑一致,仅将“收集元素”改为“收集子数组的起始/结束下标”,无需遍历元素,直接通过索引计算得到子数组的区间[start, end],效率更高。

具体逻辑
  1. 结果列表result存储int[],每个数组包含两个元素:子数组的起始下标结束下标
  2. 找到目标前缀和target后,遍历其对应的起始前缀下标startPrefix
    • 子数组起始下标 = startPrefix + 1(空前缀下标-1对应起始下标0);
    • 子数组结束下标 = 当前遍历的下标i
    • new int[]{start, end}加入结果列表;
  3. 其余逻辑与“输出子数组”完全一致,仅结果形式不同。
public List<int[]> subarrayIndices(int[] nums, int k) {
    List<int[]> result = new ArrayList<>();
    Map<Long, List<Integer>> prefixMap = new HashMap<>();
    prefixMap.put(0L, Arrays.asList(-1));
    
    long currSum = 0;
    for (int i = 0; i < nums.length; i++) {
        currSum += nums[i];
        long target = currSum - k;
        if (prefixMap.containsKey(target)) {
            for (int startPrefix : prefixMap.get(target)) {
                int start = startPrefix + 1;
                int end = i;
                result.add(new int[]{start, end});
            }
        }
        prefixMap.computeIfAbsent(currSum, x -> new ArrayList<>()).add(i);
    }
    return result;
}

总结

  1. 基础解法的核心是前缀和+哈希表:利用数学公式将子数组和问题转化为前缀和的差值问题,通过哈希表统计前缀和频次,将时间复杂度优化至O(n);
  2. 扩展解法的核心是存储前缀和的索引:在基础解法的哈希表中,将“频次”改为“索引列表”,通过索引还原子数组的区间或元素,实现从“统计个数”到“还原具体子数组”的扩展;
  3. 本题的关键优化点:
    • long存储前缀和避免溢出;
    • 初始化前缀和0对应下标-1,处理从数组起始位置开始的子数组;
    • 哈希表的核心作用是快速查找目标前缀和,避免重复计算。