【每日算法】LeetCode239. 滑动窗口最大值

LeetCode 239. 滑动窗口最大值

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

1. 题目描述

1.1 问题概要

给定一个整数数组 nums 和一个整数 k,有一个大小为 k 的滑动窗口从数组的最左侧移动到最右侧。你只可以看到在滑动窗口内的 k 个数字,滑动窗口每次只向右移动一位。返回滑动窗口中的最大值。

1.2 示例说明

输入 : nums = [1,3,-1,-3,5,3,6,7], k = 3
输出 : [3,3,5,5,6,7]
解释:

复制代码
滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

2. 问题分析

2.1 核心挑战

这个问题看似简单,实则暗藏玄机:

  • 暴力解法直观但效率低下(O(n×k))
  • 前端视角:想象你需要监控一个随时间变化的性能指标数据集,实时显示最近k个时间点的最大值
  • 关键难点:如何在窗口滑动时,高效地维护当前窗口的最大值信息,而不是每次都重新计算

2.2 前端场景联想

考虑以下前端应用场景:

  1. 性能监控面板:展示最近60秒内的最大FPS值
  2. 实时数据仪表盘:显示最近100条请求的最大响应时间
  3. 股票价格图表:实时计算最近N分钟的最高价
  4. 视差滚动效果:根据最近N个滚动位置计算动画参数

3. 解题思路

3.1 暴力解法(基础思路)

遍历每个滑动窗口,在每个窗口内找到最大值。

时间复杂度 : O(n×k)
空间复杂度 : O(1) 或 O(n-k+1) 用于存储结果
评价: 简单直观,但效率低下,不适用于大规模数据

3.2 双端队列法(最优解)

维护一个单调递减的双端队列 ,存储元素的索引而非值:

3.2.1 核心原理
  • 队列头部始终是当前窗口的最大值索引
  • 队列中的元素按照从大到小的顺序排列(对应值)
  • 当窗口滑动时:
    1. 移除队列中不在窗口范围内的索引
    2. 移除队列尾部所有小于新元素的索引(保持单调性)
    3. 将新元素索引加入队列尾部
3.2.2 算法复杂度
  • 时间复杂度: O(n) - 每个元素最多进出队列一次
  • 空间复杂度: O(k) - 双端队列最多存储k个元素
  • 最优性: 这是理论上的最优时间复杂度

3.3 分块预处理法(备选思路)

将数组分成大小为k的块,预处理每个块的前缀最大值和后缀最大值。

时间复杂度 : O(n)
空间复杂度 : O(n)
优点: 实现相对简单,适合理解分治思想

4. 代码实现

4.1 暴力解法实现

javascript 复制代码
/**
 * 暴力解法 - 直观但低效
 * 时间复杂度: O(n×k)
 * 空间复杂度: O(n-k+1)
 */
function maxSlidingWindowBruteForce(nums, k) {
    if (!nums.length || k === 0) return [];
    
    const result = [];
    const n = nums.length;
    
    for (let i = 0; i <= n - k; i++) {
        let max = -Infinity;
        for (let j = i; j < i + k; j++) {
            max = Math.max(max, nums[j]);
        }
        result.push(max);
    }
    
    return result;
}

4.2 双端队列法实现(最优解)

javascript 复制代码
/**
 * 双端队列法 - 最优解
 * 时间复杂度: O(n)
 * 空间复杂度: O(k)
 */
function maxSlidingWindowDeque(nums, k) {
    if (!nums.length || k === 0) return [];
    
    const result = [];
    const deque = []; // 存储索引的单调递减队列
    
    for (let i = 0; i < nums.length; i++) {
        // 1. 移除队列中不在窗口范围内的索引
        while (deque.length > 0 && deque[0] <= i - k) {
            deque.shift();
        }
        
        // 2. 移除队列尾部所有小于当前元素的索引
        //    保持队列单调递减(对应值)
        while (deque.length > 0 && nums[deque[deque.length - 1]] < nums[i]) {
            deque.pop();
        }
        
        // 3. 将当前索引加入队列
        deque.push(i);
        
        // 4. 当窗口形成时,将队列头部(最大值)加入结果
        if (i >= k - 1) {
            result.push(nums[deque[0]]);
        }
    }
    
    return result;
}

// 优化版本:使用双指针减少数组操作
function maxSlidingWindowOptimized(nums, k) {
    if (!nums.length || k === 0) return [];
    
    const result = [];
    const deque = new Array(k); // 预分配数组空间
    let front = 0, rear = 0; // 双指针模拟双端队列
    
    for (let i = 0; i < nums.length; i++) {
        // 移除不在窗口内的元素
        while (front < rear && deque[front] <= i - k) {
            front++;
        }
        
        // 维护单调递减性
        while (front < rear && nums[deque[rear - 1]] < nums[i]) {
            rear--;
        }
        
        // 添加当前索引
        deque[rear++] = i;
        
        // 收集结果
        if (i >= k - 1) {
            result.push(nums[deque[front]]);
        }
    }
    
    return result;
}

4.3 分块预处理法实现

javascript 复制代码
/**
 * 分块预处理法
 * 时间复杂度: O(n)
 * 空间复杂度: O(n)
 */
function maxSlidingWindowBlock(nums, k) {
    if (!nums.length || k === 0) return [];
    
    const n = nums.length;
    const prefixMax = new Array(n);
    const suffixMax = new Array(n);
    const result = new Array(n - k + 1);
    
    // 计算前缀最大值
    for (let i = 0; i < n; i++) {
        if (i % k === 0) {
            prefixMax[i] = nums[i];
        } else {
            prefixMax[i] = Math.max(prefixMax[i - 1], nums[i]);
        }
    }
    
    // 计算后缀最大值
    for (let i = n - 1; i >= 0; i--) {
        if (i === n - 1 || (i + 1) % k === 0) {
            suffixMax[i] = nums[i];
        } else {
            suffixMax[i] = Math.max(suffixMax[i + 1], nums[i]);
        }
    }
    
    // 计算每个窗口的最大值
    for (let i = 0; i <= n - k; i++) {
        const j = i + k - 1;
        result[i] = Math.max(suffixMax[i], prefixMax[j]);
    }
    
    return result;
}

5. 复杂度与优缺点对比

5.1 详细对比表格

方法 时间复杂度 空间复杂度 优点 缺点 前端应用场景
暴力解法 O(n×k) O(n-k+1) 1. 实现简单 2. 代码直观易懂 1. 效率低下 2. 不适用大数据 快速原型开发,数据量小的场景
双端队列法 O(n) O(k) 1. 时间复杂度最优 2. 空间效率高 3. 实时性好 1. 实现相对复杂 2. 需要理解单调队列 实时监控系统、大数据可视化
分块预处理法 O(n) O(n) 1. 实现相对简单 2. 可并行计算 1. 空间消耗大 2. 需要预计算 静态数据分析、批量处理

5.2 性能实测对比(模拟数据)

javascript 复制代码
// 测试不同数据规模下的性能
const testCases = [
    { n: 1000, k: 10, desc: '小数据量' },
    { n: 100000, k: 100, desc: '中等数据量' },
    { n: 1000000, k: 1000, desc: '大数据量' }
];

// 预期结果:
// 1. 小数据量:三种方法都可接受
// 2. 中等数据量:暴力法开始吃力
// 3. 大数据量:只有双端队列法高效

6. 总结与前端应用

6.1 算法精髓总结

  1. 单调队列的威力:双端队列法展示了如何通过数据结构设计将O(n×k)优化到O(n)
  2. 空间换时间思想:分块法通过预计算加速查询
  3. 前端工程师的启示:好的算法设计能显著提升用户体验,特别是在处理实时数据时

6.2 前端实际应用场景

6.2.1 性能监控系统
javascript 复制代码
// 实时监控最近N个帧率的最高值
class PerformanceMonitor {
    constructor(windowSize) {
        this.windowSize = windowSize;
        this.fpsQueue = []; // 双端队列存储时间戳
        this.maxFPS = 0;
    }
    
    recordFrame(timestamp) {
        // 移除旧帧
        while (this.fpsQueue.length > 0 && 
               timestamp - this.fpsQueue[0] > this.windowSize) {
            this.fpsQueue.shift();
        }
        
        // 添加新帧
        this.fpsQueue.push(timestamp);
        
        // 计算当前FPS
        const currentFPS = this.fpsQueue.length / (this.windowSize / 1000);
        this.maxFPS = Math.max(this.maxFPS, currentFPS);
        
        return { current: currentFPS, max: this.maxFPS };
    }
}
6.2.2 无限滚动列表优化
javascript 复制代码
// 虚拟滚动中计算可见区域的最大元素高度
class VirtualScrollOptimizer {
    constructor(containerHeight, itemHeights) {
        this.itemHeights = itemHeights;
        this.containerHeight = containerHeight;
    }
    
    // 使用滑动窗口计算渲染区域
    calculateRenderRange(scrollTop) {
        // 确定可见区域
        const startIdx = Math.floor(scrollTop / 100);
        const endIdx = Math.min(
            startIdx + Math.ceil(this.containerHeight / 100) + 5,
            this.itemHeights.length
        );
        
        // 使用滑动窗口最大值算法优化渲染
        const maxHeightInViewport = this.getMaxHeightInRange(startIdx, endIdx);
        
        return {
            startIdx,
            endIdx,
            maxHeight: maxHeightInViewport,
            buffer: 5 // 预渲染的缓冲项
        };
    }
}
6.2.3 实时图表数据流
javascript 复制代码
// 股票价格实时图表 - 计算最近N分钟最高价
class StockChart {
    constructor(timeWindow) {
        this.timeWindow = timeWindow; // 时间窗口(分钟)
        this.priceQueue = []; // {timestamp, price}
        this.maxPriceDeque = []; // 单调递减队列
    }
    
    addPrice(price, timestamp) {
        // 清理过期数据
        while (this.priceQueue.length > 0 && 
               timestamp - this.priceQueue[0].timestamp > this.timeWindow * 60000) {
            this.priceQueue.shift();
        }
        
        // 更新价格队列
        this.priceQueue.push({ price, timestamp });
        
        // 更新单调队列
        while (this.maxPriceDeque.length > 0 && 
               this.maxPriceDeque[this.maxPriceDeque.length - 1].price < price) {
            this.maxPriceDeque.pop();
        }
        this.maxPriceDeque.push({ price, timestamp });
        
        // 清理单调队列中的过期数据
        while (this.maxPriceDeque.length > 0 && 
               timestamp - this.maxPriceDeque[0].timestamp > this.timeWindow * 60000) {
            this.maxPriceDeque.shift();
        }
        
        return {
            currentPrice: price,
            maxInWindow: this.maxPriceDeque[0].price,
            prices: this.priceQueue.map(p => p.price)
        };
    }
}
相关推荐
XiaoHu02078 小时前
C++ 数据结构关于二叉搜索树
数据结构·算法
CoovallyAIHub8 小时前
下一代驾驶员监测系统如何工作?视觉AI接管驾驶舱
深度学习·算法·计算机视觉
C雨后彩虹8 小时前
事件推送问题
java·数据结构·算法·华为·面试
明洞日记8 小时前
【设计模式手册018】访问者模式 - 分离数据结构与操作
数据结构·设计模式·访问者模式
夏鹏今天学习了吗8 小时前
【LeetCode热题100(76/100)】划分字母区间
算法·leetcode·职场和发展
LYFlied9 小时前
【每日算法】LeetCode 560. 和为 K 的子数组
前端·数据结构·算法·leetcode·职场和发展
Epiphany.5569 小时前
dfn序优化树上背包
算法
fei_sun9 小时前
【数据结构】败者树、B树、排序、查找
数据结构·b树
MicroTech20259 小时前
微算法科技(NASDAQ MLGO)区块链混合检测模型优化确保全网防御策略一致性
科技·算法·区块链