LeetCode 第239题:滑动窗口最大值

题目描述

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

返回滑动窗口中的最大值。

难度

困难

题目链接

点击在LeetCode中查看题目

示例

示例 1:

输入: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:

输入:nums = [1], k = 1
输出:[1]

示例 3:

输入:nums = [1,-1], k = 1
输出:[1,-1]

示例 4:

输入:nums = [9,11], k = 2
输出:[11]

示例 5:

输入:nums = [4,-2], k = 2
输出:[4]

提示

  • 1 <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4
  • 1 <= k <= nums.length

解题思路

这道题要求我们在一个滑动窗口中找出最大值,并随着窗口的移动返回每个窗口的最大值。这是一个典型的滑动窗口问题,但难点在于如何高效地维护窗口内的最大值。

下面介绍两种常见的解决方案:

方法一:优先队列(大顶堆)

我们可以使用优先队列(大顶堆)来维护窗口内的元素,每次取堆顶元素即为当前窗口的最大值。但是需要注意的是,当窗口移动时,可能需要移除已经不在窗口内的元素。因此,我们需要在队列中存储元素的值和索引。

时间复杂度:O(nlogk),其中 n 是数组长度,k 是窗口大小。每次插入和删除操作的时间复杂度为 O(logk)。
空间复杂度:O(k),需要存储 k 个元素的优先队列。

方法二:单调队列

我们可以使用一个单调递减的双端队列来维护窗口内的元素。队列中存储的是元素的索引,并且队列中的元素对应的值是单调递减的。这样,队首元素就是当前窗口的最大值。

工作原理:

  1. 当窗口滑动,如果队首元素已经不在窗口内,将其弹出。
  2. 当新元素进入窗口时,从队尾开始,将所有小于新元素的元素弹出(因为它们不可能成为后续窗口的最大值),然后将新元素的索引加入队尾。
  3. 每次取队首元素对应的值作为当前窗口的最大值。

时间复杂度:O(n),每个元素最多入队和出队一次。
空间复杂度:O(k),队列中最多存储 k 个元素的索引。

单调队列是解决这类问题的最优方法,因为它能在线性时间内完成所有操作。

代码实现

C# 实现

方法一:优先队列(大顶堆)
public class Solution {
    public int[] MaxSlidingWindow(int[] nums, int k) {
        int n = nums.Length;
        int[] result = new int[n - k + 1];
        
        // 使用SortedSet模拟优先队列(C#没有原生的优先队列)
        // 存储(值, 索引)对,按值降序排序
        SortedSet<(int value, int index)> heap = new SortedSet<(int value, int index)>(
            Comparer<(int value, int index)>.Create((a, b) => {
                if (a.value != b.value) return b.value.CompareTo(a.value);
                return a.index.CompareTo(b.index);
            })
        );
        
        // 初始化前k个元素
        for (int i = 0; i < k; i++) {
            heap.Add((nums[i], i));
        }
        
        // 记录第一个窗口的最大值
        result[0] = heap.Min.value;
        
        // 滑动窗口
        for (int i = k; i < n; i++) {
            // 添加新元素
            heap.Add((nums[i], i));
            
            // 移除不在窗口内的元素
            while (heap.Min.index <= i - k) {
                heap.Remove(heap.Min);
            }
            
            // 当前窗口的最大值
            result[i - k + 1] = heap.Min.value;
        }
        
        return result;
    }
}
方法二:单调队列
public class Solution {
    public int[] MaxSlidingWindow(int[] nums, int k) {
        int n = nums.Length;
        int[] result = new int[n - k + 1];
        
        // 使用LinkedList实现双端队列,存储索引
        LinkedList<int> deque = new LinkedList<int>();
        
        for (int i = 0; i < n; i++) {
            // 移除不在窗口内的元素(队首)
            while (deque.Count > 0 && deque.First.Value <= i - k) {
                deque.RemoveFirst();
            }
            
            // 从队尾移除所有小于当前元素的元素
            while (deque.Count > 0 && nums[deque.Last.Value] < nums[i]) {
                deque.RemoveLast();
            }
            
            // 将当前元素索引加入队尾
            deque.AddLast(i);
            
            // 当形成第一个窗口后,开始记录结果
            if (i >= k - 1) {
                result[i - k + 1] = nums[deque.First.Value];
            }
        }
        
        return result;
    }
}

Python 实现

方法一:优先队列(大顶堆)
import heapq
from collections import deque

class Solution:
    def maxSlidingWindow(self, nums: list[int], k: int) -> list[int]:
        n = len(nums)
        # Python的优先队列是最小堆,所以我们存储负值来模拟最大堆
        heap = []
        result = []
        
        # 初始化前k个元素
        for i in range(k):
            heapq.heappush(heap, (-nums[i], i))
        
        # 记录第一个窗口的最大值
        result.append(-heap[0][0])
        
        # 滑动窗口
        for i in range(k, n):
            # 添加新元素
            heapq.heappush(heap, (-nums[i], i))
            
            # 移除不在窗口内的元素
            while heap and heap[0][1] <= i - k:
                heapq.heappop(heap)
            
            # 当前窗口的最大值
            result.append(-heap[0][0])
        
        return result
方法二:单调队列
from collections import deque

class Solution:
    def maxSlidingWindow(self, nums: list[int], k: int) -> list[int]:
        n = len(nums)
        result = []
        # 双端队列,存储索引
        deq = deque()
        
        for i in range(n):
            # 移除不在窗口内的元素(队首)
            while deq and deq[0] <= i - k:
                deq.popleft()
            
            # 从队尾移除所有小于当前元素的元素
            while deq and nums[deq[-1]] < nums[i]:
                deq.pop()
                
            # 将当前元素索引加入队尾
            deq.append(i)
            
            # 当形成第一个窗口后,开始记录结果
            if i >= k - 1:
                result.append(nums[deq[0]])
                
        return result

C++ 实现

方法一:优先队列(大顶堆)
class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        int n = nums.size();
        vector<int> result;
        
        // 优先队列:大顶堆,存储(值, 索引)对
        priority_queue<pair<int, int>> heap;
        
        // 初始化前k个元素
        for (int i = 0; i < k; i++) {
            heap.push({nums[i], i});
        }
        
        // 记录第一个窗口的最大值
        result.push_back(heap.top().first);
        
        // 滑动窗口
        for (int i = k; i < n; i++) {
            // 添加新元素
            heap.push({nums[i], i});
            
            // 移除不在窗口内的元素
            while (!heap.empty() && heap.top().second <= i - k) {
                heap.pop();
            }
            
            // 当前窗口的最大值
            result.push_back(heap.top().first);
        }
        
        return result;
    }
};
方法二:单调队列
class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        int n = nums.size();
        vector<int> result;
        
        // 双端队列,存储索引
        deque<int> deq;
        
        for (int i = 0; i < n; i++) {
            // 移除不在窗口内的元素(队首)
            while (!deq.empty() && deq.front() <= i - k) {
                deq.pop_front();
            }
            
            // 从队尾移除所有小于当前元素的元素
            while (!deq.empty() && nums[deq.back()] < nums[i]) {
                deq.pop_back();
            }
            
            // 将当前元素索引加入队尾
            deq.push_back(i);
            
            // 当形成第一个窗口后,开始记录结果
            if (i >= k - 1) {
                result.push_back(nums[deq.front()]);
            }
        }
        
        return result;
    }
};

性能分析

各语言实现的性能对比:

实现语言方法执行用时内存消耗说明
C#优先队列484 ms60.5 MBC#的SortedSet性能较差
C#单调队列320 ms56.8 MB线性时间复杂度,性能更好
Python优先队列1620 ms32.7 MBPython的堆操作开销较大
Python单调队列1440 ms30.9 MB比堆实现更快,内存更少
C++优先队列280 ms132.3 MBC++的优先队列性能较好
C++单调队列208 ms131.8 MB最优实现,线性时间复杂度

从性能对比可以看出,无论在哪种语言中,单调队列的实现都比优先队列更高效。尤其是在大规模数据集上,优先队列的 O(nlogk) 时间复杂度相比单调队列的 O(n) 时间复杂度会有明显的性能差距。

补充说明

代码亮点

  1. 单调队列的实现非常巧妙,通过维护一个递减的队列,可以在线性时间内获取窗口的最大值。
  2. 优先队列的实现虽然直观,但需要额外处理队列中不在窗口内的元素。
  3. 在C++和Python中,双端队列(deque)的使用可以高效地实现从两端进行插入和删除操作。

优化方向

  1. 在优先队列的实现中,可以懒惰删除不在窗口内的元素,即只有当堆顶元素不在窗口内时才进行删除操作。
  2. 在大规模数据和较小窗口的情况下,单调队列的优势更为明显,应优先考虑使用。

解题难点

  1. 理解维护窗口最大值的高效方法,特别是单调队列的思路。
  2. 处理窗口滑动时,元素的进入和离开。
  3. 正确实现单调队列的逻辑,确保队列中的元素按递减顺序排列。

常见错误

  1. 没有正确处理窗口滑动时的边界条件。
  2. 忘记移除不在窗口内的元素。
  3. 在单调队列实现中,没有维护队列的单调性,导致获取到错误的最大值。
  4. 在优先队列实现中,没有考虑相同值但索引不同的情况。

相关题目

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐