栈与队列系列④ -- 滑动窗口的最大值

目录

  • 题目概述
  • 解题思路
  • 代码实现
  • 此题的几个重要知识点
    • 逻辑表达式
    • 将Integer列表转化为int数组
    • 三元表达式的复习

题目概述

此题对应力扣的239.滑动窗口的最大值
题目:

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

示例:
栈与队列系列④ -- 滑动窗口的最大值_第1张图片

解题思路

这个题刚开始做的时候,目的很明确使用链表模拟队列,然后随着窗口的移动进行相应的进队和出队,然后再利用Collections的方法max()求最大值。不过放在力扣上一运行超时了。
代码如下:

public int[] maxSlidingWindow(int[] nums, int k) {
        //创建结果集
        LinkedList result = new LinkedList<>();
        //创建单指针
        int end = k-1;
        // 创建队列,并将窗口内的数据装入
        LinkedList window = new LinkedList<>();
        for(int i = 0;i < k;i++){
            window.add(nums[i]);
        }
        int max ;
        while(end < nums.length){
            max = Collections.max(window);
            result.add(max);
            window.remove();
            if(end == nums.length - 1){
                break;
            }
            end++;
            window.add(nums[end]);
        }
        return  result.stream().mapToInt(Integer::valueOf).toArray();
    }

原因是时间复杂度为O(n2),因为这个Collections的max()方法的时间复杂度就已经是O(n)了。后来我想怎么去简单的求解一个区域里的最大值呢,试了很多方法都不怎么行。

其实这个题的核心思想就是:不保证滑动窗口里的每一个值,但要保证滑动窗口里的可能最大值。

那么怎么去实现呢?我们的理想状态就是说,让这个队列呈现单调的趋势(也就是单调队列),最大值在出口处,滑动窗口每移动一次就把出口处的最大值给弹出去,然后再用一个结果集给收集起来,就大功告成了。想要到达这个理想状态,我们只需要将添加方法,和弹出方法自定义一下即可。

添加方法:每向队列尾端添加元素的时候,都与队尾的元素进行比较,如果队尾的元素比要添加进来的元素小,就把队尾的元素给弹出来,如此一直直到队尾的元素大于等于要添加进来的元素,再将元素从尾部添加即可(前提是队列不为空)。如此即可保持队列的单调型

弹出方法:当滑动窗口进行移动的时候,要弹出元素,每次要进行弹出的时候都要判断,如果要弹出的这个元素与队列的头部元素(也就是此时队列的最大值)相同,就把队列的首部元素弹出;如果不等于,就不用管。(为什么这么做?这就体现了只对最大值的保护性,也就是如果弹出的跟你这个当前最大值没关系,那就不动他。如果随着滑动窗口的移动弹出来的就是这个最大值,那就要把他弹出来,此时的最大值自然要发生变化。)

窗口每移动一次就将队列头的最大值放入结果集即可。

代码实现

public class LeetCode239Pro {
    Deque<Integer> window = new LinkedList<>();
    // 此法用双端队列模拟单调队列
    public int[] maxSlidingWindow(int[] nums, int k) {
        //创建结果集
        LinkedList<Integer> result = new LinkedList<>();
        // 将前k个元素先放入双端队列中
        for(int i = 0;i < k;i++){
            tianJia(nums[i]);
        }
        result.add(window.getFirst());
        //窗口开始移动
        for(int end = k;end < nums.length;end++){
            tanChu(nums[end - k]);
            tianJia(nums[end]);
            result.add(window.getFirst());
        }
        return result.stream().mapToInt(Integer::valueOf).toArray();

    }
    // 添加方法
    // 从尾端添加,添加的时候与尾部的元素进行大小比较,如果比尾部元素大,
    // 就把尾部的元素弹出来,直到小于等于尾部元素即可。
    public void tianJia(int num){
        while(!window.isEmpty() &&  num > window.getLast()  ){
            window.pollLast();
        }
        window.addLast(num);
    }
    // 弹出方法
    //弹出的时候与队列的头部元素进行的大小比较,如果相同则弹出,不相同就不用管。
    // 中心思想:只维护最大值,但不维护窗口里的所有值
    public void tanChu(int num){
        if(window.getFirst() == num && !window.isEmpty()){
            window.remove();
        }
    }
}

此题的几个重要知识点

逻辑表达式

在写添加方法的时候,程序一直给我报java.util.NoSuchElementException,这就让我非常疑惑,琢磨了半天,最后发现问题就出在一个不容易发现的点上:
代码一:

while (!deque.isEmpty() && val > deque.getLast()) {
    deque.removeLast();
}

代码二:

while (val > deque.getLast() && !deque.isEmpty() ) {
    deque.removeLast();
}

这两个代码的区别就在我把&&前后两个布尔表达式换了一下位置,这就造成了代码一是可以运行的,而代码二是不能运行的。

这其中涉及到了两个知识点:

  • && 与 & 的区别
  • 双端队列中getLast()方法的特点

首先&&相对于&有个特点:短路,也就是说&&左边的布尔表达式的值一旦等于false,那么右边的是什么东西压根不会看,更不会去执行。如果基础不够扎实,心中总会存在这两个差不多无所谓用哪个的错误想法,因为平时编程序的时候也没报错,但如果一旦涉及这种错误根本找不到。
双端队列中的getLast()方法是会抛异常的,在队列为空的时候。

如此就可以知道为什么代码二会报错了。因为如果getLast()方法一旦报错,这个程序是一定不能继续执行的。而代码一能够执行,是因为他优先判断了队列是否为空,如果不为空它才会执行后面的,而如果为空,他压根就不会再执行后面的代码,getLast()也就不存在报错的可能性

这种错误以后写代码的时候时一定要注意的。

将Integer列表转化为int数组

如果用for循环一个个去转换再放入一个新的数组会让代码看起来冗余,这里可以用流的转换。
通用格式:

return 列表名.stream().mapToInt(Integer::valueOf).toArray();

三元表达式的复习

用的少所以爱忘:
基本结构:
变量 = 布尔表达式 ? 如果布尔表达式为true的值:如果布尔表达式为false的值

高级用法:三元表达式也可以嵌套,例如两个值处均可以用三元表达式进行嵌套。

你可能感兴趣的:(LeetCode,java,leetcode,算法)