[算法手记] 滑动窗口最大值

分享一下刷题记录

滑动窗口的最大值

1.题目链接

题目链接 : https://leetcode.cn/problems/sliding-window-maximum/description/?envType=study-plan-v2\&envId=top-100-liked

2. 题目分析

题目不难理解 ,让一个固定长度为k的窗口在数组中从左往右每次滑动一位, 返回每次窗口中元素的最大值

使用滑动窗口的思路来解决的话, 我们只需要维护一个最大值Max即可, 每次进窗口时更新把新加入的元素和Max进行比较,但是这道题之所以成为困难题就是出现在出窗口这个逻辑上

出窗口时, 窗口内元素一进一出, 这时候如何维护Max的值又成了一个问题,

  • 如果窗口left指针出窗口的元素不是最大值的话还好, 因为此时最大值还在窗口中,还处于有效的状态, 新入窗口的元素仍旧可以和它进行比较
  • 如果恰好出窗口的元素刚好就是原来维护的Max时, Max就变为无效元素了, 我们就需要找出在移动之后的窗口内的最大元素, 把它的值赋于Max, 如果使用暴力的解法的话, 时间复杂度则为O(k* n),在窗口长度过大, nums数组元素过多时都会导致超时
    下面附上暴力解法:
java 复制代码
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        List<Integer> list = new ArrayList<>();
        int n = nums.length, l = 0, r = 0;
        if (n == 1) return new int[]{nums[0]};
        
        int sum = Integer.MIN_VALUE;
        for (; r < n; r++) {
            sum = Math.max(sum, nums[r]);
            // 窗口满
            if (r - l + 1 == k) {
                list.add(sum); // 1. 先把当前窗口的最大值存起来

                // 2. 准备让 l 右移。在右移前,检查即将出窗口的 nums[l] 是否是最大值
                if (nums[l] == sum) {
                    // 如果是,我们需要在 [l+1, r] 这个范围内重新找一个最大值
                    sum = Integer.MIN_VALUE;
                    for (int i = l + 1; i <= r; i++) {
                        sum = Math.max(sum, nums[i]);
                    }
                }
                l++; // 3. 左边界正式右移
            }
        }
        return list.stream().mapToInt(Integer::intValue).toArray();
    }
}

最终也是不出所料的超出时间限制

使用单调队列就可以很好的解决这个问题,什么是单调队列呢

3. 单调队列

1. 核心特性

单调队列不仅仅是普通队列(FIFO),它允许从队尾进行元素的删除操作。其核心逻辑如下:

  • 队列性质 :队列中的元素值必须满足单调性(如:若要求最大值,则队列内元素从队头到队尾必须是严格单调递减的)。

  • 元素入队(维持单调性) :当一个新的元素 xxx 进入窗口时:

    1. 如果 xxx 大于队尾元素,那么队尾元素永远不可能成为当前或后续窗口的最大值,直接将其弹出。

    2. 重复上述操作,直到队尾元素大于 xxx 或队列为空,然后将 xxx 入队。

  • 元素出队(过期处理) :由于窗口大小限制为 KKK,如果队头元素已经滑出当前窗口范围(索引小于 当前索引 - K + 1),则将其弹出。

2. 为什么使用它?

  • 时间复杂度 :每个元素最多进队一次、出队一次,因此总时间复杂度为 O(N)O(N)O(N) 。相比于暴力求解的 O(N×K)O(N \times K)O(N×K),在大数据量下效率极高。

  • 空间复杂度 :O(K)O(K)O(K),最多存储 KKK 个元素。

3.为什么不使用大根堆?

本题的难点在于如何找出移动之后窗口内的最大值,最开始我试图使用大根堆来处理? 来维护一个元素个数为k的大根堆, 但是很快就发现是行不通的,虽然大根堆可以很方便的获取到窗口内的最大值, 但是堆内部的元素顺序和窗口内的元素顺序是不一致的, 这就导致了我们虽然在进窗口时会堆化把最大值置于堆顶,但是出窗口时却需要遍历整个堆来找到要出窗口的那个元素, 所以使用大根堆是行不通的

但是单调队列内部是排资论辈的, 新来的如果比队尾小, 就不会加入队列中, 如果比队尾大则弹出队尾元素取而代之, 这就保证了队尾元素一定是最新加入的元素, 而队首元素一定是最大,资历最老的元素,可以有效的维护窗口在滑动过程中的Max值

4.代码参考(单调队列)

java 复制代码
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        if(n == 1) return new int []{nums[0]};
        int []ret = new int [n - k + 1];
        int retIndex = 0;
        
        //使用单调队列
        Deque<Integer> deque = new ArrayDeque<>();

        for(int r = 0;r < n;r++){
            //检查队头元素,排除最大值是否过期
            if(!deque.isEmpty() && deque.peekFirst() < r - k + 1){//队首最大值该出窗口了
                deque.pollFirst();
            }

            //新加入窗口的比队尾元素大
            while(!deque.isEmpty() && nums[deque.peekLast()] < nums[r]){
                deque.pollLast();//队尾弹出
            }
            //较大值的新元素加入队尾
            deque.offerLast(r);

            //窗口大小达到k时记录结果
            if(r >= k - 1){
                ret[retIndex++] = nums[deque.peekFirst()];
            }
        }
        return ret;
    }
}
相关推荐
洛水水1 小时前
【力扣100题】82.有效的括号
c++·算法·leetcode
XGeFei1 小时前
时序算法 —— LSTM、ARIMA、随机森林
算法·随机森林·lstm
湖南天硕国产SSD1 小时前
工业存储可靠性进阶:天硕工业固态硬盘动态温控与寿命优化技术实践
网络·数据库·算法·工业存储·天硕存储·工业固态硬盘
legend050709ComeON1 小时前
常见面试题-leetcode
数据结构·算法·leetcode
Smilecoc1 小时前
决策树(一):决策树基本原理
算法·决策树·机器学习
weixin_307779131 小时前
从工具到协作者:AI在后端研发中的流程重构与组织赋能
人工智能·后端·python·算法·自动化
沉下去,苦磨练!2 小时前
深度学习神经网络的搭建
人工智能·算法
孬甭_2 小时前
深入剖析快速排序:原理、实现与性能优化
数据结构·算法·排序算法
阿正的梦工坊2 小时前
【Rust】06-函数、控制流与模块组织
开发语言·算法·rust