滑动窗口的最大值(双端队列最优解+多语言实现)

在算法面试中,“滑动窗口的最大值”是高频考点之一,它不仅考察对滑动窗口思想的理解,更考验对数据结构(如双端队列)的灵活运用。本文将基于《剑指Offer》原题,从问题分析、思路推导、算法优化到多语言实现,全方位拆解这道题,帮助你不仅会“做”,更能理解“为什么这么做”。

一、问题重述与核心难点

1. 题目定义

给定一个长度为n的整数数组num和滑动窗口大小size,窗口从数组最左侧向右滑动,每次滑动1个位置,求所有滑动窗口内的最大值。
示例
输入数组[2,3,4,2,6,2,5,1],窗口大小3,输出[4,4,6,6,6,5]
对应的6个滑动窗口如下:

滑动窗口

最大值

[2,3,4],2,6,2,5,1

4

2,[3,4,2],6,2,5,1

4

2,3,[4,2,6],2,5,1

6

2,3,4,[2,6,2],5,1

6

2,3,4,2,[6,2,5],1

6

2,3,4,2,6,[2,5,1]

5

2. 核心难点

若采用“暴力解法”(每次窗口滑动后遍历窗口内元素找最大值),时间复杂度为O(n*size)——当nsize较大时(如n=1e5size=1e3),会出现超时问题。
因此,如何降低时间复杂度是本题的关键,目标是找到一种能在O(1)时间内获取窗口最大值、O(n)时间内完成所有窗口处理的方案。

二、最优思路:双端队列(Deque)的“单调递减”应用

1. 核心思想

双端队列(Deque)支持两端的插入和删除操作,我们可以利用它维护一个“单调递减序列”,队列中存储的是数组元素的下标(而非元素值),满足以下规则:

  1. 入队规则:对于当前元素num[i],从队列尾部开始,删除所有小于num[i]的元素下标——因为这些元素在num[i]存在的窗口中,永远不可能成为最大值(num[i]更大且更靠后,滑动时会更晚离开窗口)。

  2. 出队规则:若队列头部的下标超出当前窗口的左边界(即index.front() <= i - size),说明该元素已滑出窗口,需从头部删除。

  3. 最大值获取:每个窗口的最大值对应队列头部的元素值(num[index.front()])——因为队列是单调递减的,头部始终是当前窗口的最大元素下标。

2. 分步推导(结合示例)

以数组[2,3,4,2,6,2,5,1]、窗口大小3为例,逐步拆解双端队列的操作过程:

步骤

当前元素

当前窗口范围

队列内下标(对应值)

操作说明

窗口是否完整

最大值

1

2(i=0)

[0,0]

[0(2)]

队列空,直接入队

-

2

3(i=1)

[0,1]

[1(3)]

3>2,删除尾部的0,入队1

-

3

4(i=2)

[0,2]

[2(4)]

4>3,删除尾部的1,入队2

4(num[2])

4

2(i=3)

[1,3]

[2(4),3(2)]

2<4,直接入队3

4(num[2])

5

6(i=4)

[2,4]

[4(6)]

6>2、6>4,删除3、2,入队4

6(num[4])

6

2(i=5)

[3,5]

[4(6),5(2)]

2<6,直接入队5

6(num[4])

7

5(i=6)

[4,6]

[4(6),6(5)]

5>2,删除5,入队6

6(num[4])

8

1(i=7)

[5,7]

[6(5),7(1)]

4已滑出窗口(4 <=7-3=4?是,删除4);1<5,入队7

5(num[6])

最终收集的最大值为[4,4,6,6,6,5],与预期结果一致。

3. 时间复杂度分析

  • 每个元素最多入队1次、出队1次,队列操作的总时间为O(n)

  • 遍历数组的时间为O(n)

  • 整体时间复杂度为O(n),空间复杂度为O(size)(队列最多存储size个元素)。

三、多语言实现(附详细注释)

1. C++ 实现(基于STL deque)

#include <vector>
#include <deque>
#include <iostream>
using namespace std;

class Solution {
public:
    vector<int> maxInWindows(const vector<int>& num, unsigned int size) {
        vector<int> maxInWindows;  // 存储所有窗口的最大值
        deque<int> indexQueue;     // 双端队列,存储数组元素的下标(维护单调递减)

        // 边界条件:数组长度需>=窗口大小,且窗口大小>=1
        if (num.empty() || size < 1 || num.size() < size) {
            return maxInWindows;
        }

        // 第一步:初始化第一个窗口(前size个元素)
        for (unsigned int i = 0; i < size; ++i) {
            // 从队尾删除所有小于当前元素的下标(保证队列单调递减)
            while (!indexQueue.empty() && num[i] >= num[indexQueue.back()]) {
                indexQueue.pop_back();
            }
            // 当前元素下标入队
            indexQueue.push_back(i);
        }

        // 第二步:处理后续窗口(从第size个元素开始)
        for (unsigned int i = size; i < num.size(); ++i) {
            // 记录当前窗口的最大值(队列头部元素)
            maxInWindows.push_back(num[indexQueue.front()]);

            // 1. 维护队列单调性:删除队尾小于当前元素的下标
            while (!indexQueue.empty() && num[i] >= num[indexQueue.back()]) {
                indexQueue.pop_back();
            }

            // 2. 移除滑出窗口的元素下标(头部元素超出窗口左边界)
            if (!indexQueue.empty() && indexQueue.front() <= static_cast<int>(i - size)) {
                indexQueue.pop_front();
            }

            // 3. 当前元素下标入队
            indexQueue.push_back(i);
        }

        // 第三步:添加最后一个窗口的最大值(循环中未处理)
        maxInWindows.push_back(num[indexQueue.front()]);

        return maxInWindows;
    }
};

// 测试代码
int main() {
    Solution sol;
    vector<int> num = {2, 3, 4, 2, 6, 2, 5, 1};
    unsigned int size = 3;
    vector<int> result = sol.maxInWindows(num, size);

    // 输出结果:4 4 6 6 6 5
    for (int val : result) {
        cout << val << " ";
    }
    cout << endl;
    return 0;
}

2. Python 实现(基于collections.deque)

Python的collections.deque提供了双端队列的功能,操作与C++类似,代码更简洁:

from collections import deque
from typing import List

class Solution:
    def maxInWindows(self, num: List[int], size: int) -> List[int]:
        max_in_windows = []  # 存储结果
        index_queue = deque()  # 双端队列,存下标(单调递减)

        # 边界条件判断
        if not num or size < 1 or len(num) < size:
            return max_in_windows

        # 初始化第一个窗口
        for i in range(size):
            # 队尾删除小于当前元素的下标
            while index_queue and num[i] >= num[index_queue[-1]]:
                index_queue.pop()
            index_queue.append(i)

        # 处理后续窗口
        for i in range(size, len(num)):
            # 记录当前窗口最大值
            max_in_windows.append(num[index_queue[0]])

            # 1. 维护队列单调性
            while index_queue and num[i] >= num[index_queue[-1]]:
                index_queue.pop()

            # 2. 移除滑出窗口的下标
            if index_queue and index_queue[0] <= i - size:
                index_queue.popleft()

            # 3. 当前下标入队
            index_queue.append(i)

        # 添加最后一个窗口的最大值
        max_in_windows.append(num[index_queue[0]])

        return max_in_windows

# 测试
if __name__ == "__main__":
    sol = Solution()
    num = [2, 3, 4, 2, 6, 2, 5, 1]
    size = 3
    print(sol.maxInWindows(num, size))  # 输出:[4, 4, 6, 6, 6, 5]

3. Java 实现(基于LinkedList模拟双端队列)

Java中无专门的Deque类,但可通过LinkedListaddLast()removeLast()peekFirst()等方法模拟双端队列:

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class Solution {
    public List<Integer> maxInWindows(int[] num, int size) {
        List<Integer> maxInWindows = new ArrayList<>();
        LinkedList<Integer> indexQueue = new LinkedList<>();  // 模拟双端队列

        // 边界条件
        if (num == null || num.length == 0 || size < 1 || num.length < size) {
            return maxInWindows;
        }

        // 初始化第一个窗口
        for (int i = 0; i < size; i++) {
            // 队尾删除小于当前元素的下标
            while (!indexQueue.isEmpty() && num[i] >= num[indexQueue.peekLast()]) {
                indexQueue.removeLast();
            }
            indexQueue.addLast(i);
        }

        // 处理后续窗口
        for (int i = size; i < num.length; i++) {
            // 记录当前窗口最大值
            maxInWindows.add(num[indexQueue.peekFirst()]);

            // 1. 维护队列单调性
            while (!indexQueue.isEmpty() && num[i] >= num[indexQueue.peekLast()]) {
                indexQueue.removeLast();
            }

            // 2. 移除滑出窗口的下标
            if (!indexQueue.isEmpty() && indexQueue.peekFirst() <= i - size) {
                indexQueue.removeFirst();
            }

            // 3. 当前下标入队
            indexQueue.addLast(i);
        }

        // 添加最后一个窗口的最大值
        maxInWindows.add(num[indexQueue.peekFirst()]);

        return maxInWindows;
    }

    // 测试
    public static void main(String[] args) {
        Solution sol = new Solution();
        int[] num = {2, 3, 4, 2, 6, 2, 5, 1};
        int size = 3;
        System.out.println(sol.maxInWindows(num, size));  // 输出:[4, 4, 6, 6, 6, 5]
    }
}

四、常见问题与优化拓展

1. 边界条件处理

必须考虑以下特殊情况,否则会出现逻辑错误或空指针异常:

  • 数组为空(num.length == 0);

  • 窗口大小为0或1(size < 1时返回空,size=1时直接返回原数组);

  • 数组长度小于窗口大小(num.length < size,无有效窗口,返回空)。

2. 为什么存储“下标”而非“元素值”?

若存储元素值,无法判断该元素是否已滑出窗口(例如窗口中存在多个相同值,无法区分位置)。存储下标可通过index <= i - size直接判断是否超出窗口范围,逻辑更严谨。

3. 拓展场景:滑动窗口的最小值

若题目改为“求滑动窗口的最小值”,只需将双端队列的“单调递减”改为“单调递增”——队列头部始终是当前窗口的最小元素下标,入队时删除所有大于当前元素的下标即可。

五、总结

“滑动窗口的最大值”的核心是用双端队列维护单调序列,通过牺牲少量空间(O(size))换取时间复杂度的优化(从O(n*size)降至O(n)),是典型的“空间换时间”算法思想。
掌握该思路后,不仅能解决本题,还能应对类似的“滑动窗口极值”问题(如最小值、第k大值等),在面试中做到举一反三。建议结合上述代码手动推导一遍队列操作过程,加深对“单调队列”的理解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值