【每日算法】LeetCode 560. 和为 K 的子数组

对前端开发者而言,学习算法绝非为了"炫技"。它是你从"页面构建者"迈向"复杂系统设计者"的关键阶梯。它将你的编码能力从"实现功能"提升到"设计优雅、高效解决方案"的层面。从现在开始,每天投入一小段时间,结合前端场景去理解和练习,你将会感受到自身技术视野和问题解决能力的质的飞跃。------ 算法:资深前端开发者的进阶引擎

LeetCode 560. 和为 K 的子数组:前缀和的精妙应用

1. 题目描述

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

示例 1:

复制代码
输入:nums = [1,1,1], k = 2
输出:2
解释:[1,1] 与 [1,1] 为两种不同的情况(注意:虽然值相同,但索引位置不同)

示例 2:

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

约束条件:

  • 数组长度范围:1 <= nums.length <= 2 * 10⁴
  • 数组元素范围:-1000 <= nums[i] <= 1000
  • k 的范围:-10⁷ <= k <= 10⁷

2. 问题分析

2.1 问题核心

这个问题要求统计数组中连续子数组 的和等于给定值 k 的个数。这是一个典型的子数组求和问题,需要特别注意:

  • 子数组必须是连续的
  • 数组元素可以是负数,这增加了问题的复杂性
  • 需要考虑空数组吗?题目没有明确说明,但根据示例,至少需要包含一个元素

2.2 前端视角的类比

在前端开发中,类似的问题场景包括:

  • 统计页面中连续点击次数达到特定阈值的情况
  • 计算用户行为序列中特定模式的出现次数
  • 分析性能监控数据中连续时间段内指标达标的情况

3. 解题思路

3.1 思路演进

3.1.1 暴力枚举法

最直观的想法是枚举所有可能的子数组,计算它们的和,统计等于 k 的数量。

3.1.2 前缀和优化

暴力法的时间复杂度为 O(n²),对于 2×10⁴ 的数据量会超时。我们需要更高效的方法。

前缀和(Prefix Sum)概念

前缀和是一种预处理技术,通过预先计算并存储数组的前缀和,可以在 O(1) 时间内计算任意子数组的和。

定义前缀和数组 preSum,其中 preSum[i] 表示 nums[0] + nums[1] + ... + nums[i-1] 的和。

那么子数组 nums[i..j] 的和可以表示为:

复制代码
sum(nums[i..j]) = preSum[j+1] - preSum[i]
3.1.3 哈希表优化

对于每个 j,我们需要找到有多少个 i 满足 preSum[i] = preSum[j+1] - k。使用哈希表可以在 O(1) 时间内完成查找。

核心公式

复制代码
preSum[j+1] - preSum[i] = k
=> preSum[i] = preSum[j+1] - k

3.2 复杂度分析

方法 时间复杂度 空间复杂度 是否推荐
暴力枚举 O(n²) O(1) 不推荐,会超时
前缀和+哈希表 O(n) O(n) 推荐,最优解

4. 各思路代码实现

4.1 暴力枚举法(不推荐,仅用于理解)

javascript 复制代码
/**
 * 暴力枚举法
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var subarraySumBruteForce = function(nums, k) {
    let count = 0;
    const n = nums.length;
    
    for (let i = 0; i < n; i++) {
        let sum = 0;
        for (let j = i; j < n; j++) {
            sum += nums[j];
            if (sum === k) {
                count++;
            }
        }
    }
    
    return count;
};

4.2 前缀和+哈希表法(最优解)

javascript 复制代码
/**
 * 前缀和+哈希表法(最优解)
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var subarraySum = function(nums, k) {
    // 哈希表:键为前缀和,值为该前缀和出现的次数
    const map = new Map();
    // 初始化:前缀和为0出现了1次(空数组的情况)
    map.set(0, 1);
    
    let count = 0;
    let prefixSum = 0;
    
    for (let i = 0; i < nums.length; i++) {
        // 计算当前前缀和
        prefixSum += nums[i];
        
        // 如果存在某个前缀和等于 currentPrefixSum - k
        // 说明从那个位置到当前位置的子数组和为 k
        if (map.has(prefixSum - k)) {
            count += map.get(prefixSum - k);
        }
        
        // 更新当前前缀和出现的次数
        map.set(prefixSum, (map.get(prefixSum) || 0) + 1);
    }
    
    return count;
};

4.3 带详细注释的版本(便于理解)

javascript 复制代码
/**
 * 前缀和+哈希表法(详细注释版)
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var subarraySumWithComments = function(nums, k) {
    // 哈希表:存储前缀和及其出现次数
    // 为什么需要这个哈希表?
    // 我们要找的是:prefixSum[j] - prefixSum[i] = k
    // 即:prefixSum[i] = prefixSum[j] - k
    // 所以对于每个j,我们需要知道之前有多少个i满足这个条件
    const prefixSumCount = new Map();
    
    // 为什么要初始化前缀和为0出现了1次?
    // 考虑整个数组从开头到某个位置的子数组和为k的情况
    // 即:prefixSum[j] - 0 = k,这时候prefixSum[i]为0(i为-1,空数组)
    prefixSumCount.set(0, 1);
    
    let currentSum = 0;  // 当前前缀和
    let result = 0;      // 结果计数
    
    for (let i = 0; i < nums.length; i++) {
        // 计算到当前位置的前缀和
        currentSum += nums[i];
        
        // 核心逻辑:如果存在一个前缀和等于 currentSum - k
        // 那么从那个位置到当前位置的子数组和就是k
        // 例如:nums = [1, 2, 3], k = 3
        // 当i=2时,currentSum = 6
        // currentSum - k = 3,如果之前出现过前缀和3,那么从那个位置到当前位置的和就是3
        const target = currentSum - k;
        
        if (prefixSumCount.has(target)) {
            // 可能有多个位置的前缀和都等于target,所以都要加上
            result += prefixSumCount.get(target);
        }
        
        // 更新当前前缀和的出现次数
        // 这里使用 || 0 来处理undefined的情况(如果该前缀和之前没出现过)
        prefixSumCount.set(
            currentSum, 
            (prefixSumCount.get(currentSum) || 0) + 1
        );
    }
    
    return result;
};

5. 各实现思路的复杂度、优缺点对比

5.1 对比表格

实现方法 时间复杂度 空间复杂度 优点 缺点 适用场景
暴力枚举法 O(n²) O(1) 1. 思路直观简单 2. 不需要额外空间 1. 效率低下,n=20000时会超时 2. 不适合大数据量 小规模数据(n<1000)或教学演示
前缀和+哈希表 O(n) O(n) 1. 时间复杂度最优 2. 能处理包含负数的情况 3. 适合大规模数据 1. 需要额外O(n)空间 2. 逻辑相对复杂 大规模数据处理、生产环境推荐使用

5.2 详细分析

5.2.1 暴力枚举法
  • 时间复杂度分析

    • 外层循环:n次
    • 内层循环:平均n/2次
    • 总复杂度:O(n²)
    • 当n=20000时,操作次数约2亿次,明显超时
  • 空间复杂度:O(1),只使用了常数级别的额外空间

5.2.2 前缀和+哈希表法
  • 时间复杂度分析

    • 单次遍历数组:O(n)
    • 每次操作哈希表:平均O(1)
    • 总复杂度:O(n)
    • 当n=20000时,操作次数约2万次,效率极高
  • 空间复杂度

    • 最坏情况下,每个前缀和都不同,需要存储n个键值对
    • 空间复杂度:O(n)

6. 总结与前端应用场景

6.1 核心要点总结

  1. 前缀和思想:将子数组求和问题转化为前缀和之差的问题
  2. 哈希表优化:通过哈希表记录前缀和出现次数,实现O(1)时间查找
  3. 边界处理:注意初始化前缀和为0的情况(对应空子数组)
  4. 负数处理:由于数组元素可能为负数,不能使用双指针滑动窗口法

6.2 实际应用场景(前端视角)

6.2.1 性能监控与分析
javascript 复制代码
// 监控连续时间段内API错误率达到阈值的情况
const errorRates = [0.1, 0.2, 0.05, 0.3, 0.15, 0.25];
const threshold = 0.5;

// 统计连续时间段内错误率总和超过阈值的时间段数量
function countErrorSpikes(errorRates, threshold) {
    const map = new Map();
    map.set(0, 1);
    
    let count = 0;
    let prefixSum = 0;
    
    for (let rate of errorRates) {
        prefixSum += rate;
        if (map.has(prefixSum - threshold)) {
            count += map.get(prefixSum - threshold);
        }
        map.set(prefixSum, (map.get(prefixSum) || 0) + 1);
    }
    
    return count;
}
6.2.2 用户行为分析
javascript 复制代码
// 分析用户连续操作序列
// 例如:统计用户连续点击次数达到特定模式的情况
const userActions = ['click', 'scroll', 'click', 'hover', 'click'];
const targetPattern = 2; // 连续click的次数

function countActionPatterns(actions, targetAction, targetCount) {
    const map = new Map();
    map.set(0, 1);
    
    let count = 0;
    let currentStreak = 0;
    
    for (let action of actions) {
        // 如果是目标行为,streak加1,否则重置为-1(或其他负值)
        currentStreak += (action === targetAction ? 1 : -1);
        
        // 查找是否有位置使得连续目标行为次数等于targetCount
        if (map.has(currentStreak - targetCount)) {
            count += map.get(currentStreak - targetCount);
        }
        
        map.set(currentStreak, (map.get(currentStreak) || 0) + 1);
    }
    
    return count;
}
6.2.3 数据处理与可视化
javascript 复制代码
// 在数据可视化中,统计连续时间段内数据超过阈值的情况
class DataAnalyzer {
    constructor() {
        this.prefixSumMap = new Map();
    }
    
    // 实时数据流处理
    processDataStream(dataStream, threshold) {
        const result = [];
        let prefixSum = 0;
        let map = new Map([[0, [-1]]]); // 存储前缀和及其出现的位置
        
        for (let i = 0; i < dataStream.length; i++) {
            prefixSum += dataStream[i];
            
            // 查找满足条件的位置
            const target = prefixSum - threshold;
            if (map.has(target)) {
                const positions = map.get(target);
                for (let pos of positions) {
                    result.push([pos + 1, i]); // 子数组的起始和结束索引
                }
            }
            
            // 更新哈希表
            if (!map.has(prefixSum)) {
                map.set(prefixSum, []);
            }
            map.get(prefixSum).push(i);
        }
        
        return result;
    }
}
相关推荐
howcode8 小时前
年度总结——Git提交量戳破了我的副业窘境
前端·后端·程序员
恋猫de小郭8 小时前
OpenAI :你不需要跨平台框架,只需要在 Android 和 iOS 上使用 Codex
android·前端·openai
Epiphany.5568 小时前
dfn序优化树上背包
算法
fei_sun8 小时前
【数据结构】败者树、B树、排序、查找
数据结构·b树
全马必破三8 小时前
浏览器原理知识点总结
前端·浏览器
零Suger8 小时前
React 组件通信
前端·react.js·前端框架
MicroTech20258 小时前
微算法科技(NASDAQ MLGO)区块链混合检测模型优化确保全网防御策略一致性
科技·算法·区块链