Leetcode.239. 滑动窗口最大值---暴力/优化暴力/单调队列

239. 滑动窗口最大值

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

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

示例 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 <= 105
-104 <= nums[i] <= 104
1 <= k <= nums.length

题解:

方法一:暴力

  • 我们直接维护一个滑动窗口,接着按照要求进行poppush的操作即可,但是由于每次都要求一次滑窗里的max,所以此法很耗费时间。
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        List list = new ArrayList();
        int[] res = new int[nums.length];
        int index = 0;
        for(int i=0;i<nums.length;i++){
            if(list.size()<k){
                list.add(nums[i]);
            }
            else{
                res[index++] = cal(list);
                list.remove((int)0);
                list.add(nums[i]);
            }
        }

        res[index++] = cal(list);

        int[] temp = new int[index];
        for(int i=0;i<index;i++){
            temp[i] = res[i];
        }
        return temp;
    }

    public int cal(List list){
        int res = -20000;
        for(int i=0;i<list.size();i++){
            res = Math.max(res , (int)(list.get((int)i)));
        }

        return res;
    }
}

在这里插入图片描述

方法二:优化暴力

  • 在方法一的基础上,增加存储滑窗max值的设计,即在滑窗移动后求新的max时不是直接计算新的max,而是判断一下原先的max是否被移出滑窗,如果没有则继续用这个max,接着再判断一下要加进来的元素和原max之间的大小关系,如果要进滑窗的元素直接大于原max,则原max直接禅位即可。其实和下面法三有异曲同工之妙,在于维护滑窗的同时,维护了一个仅存储滑窗max值及其所处滑窗位置的res数组
  • 需要注意的是在cal函数中之所以要传入flag这个布尔类型数组,是因为值传递的话修改的只是形参,没有真正的修改值,所以这里使用数组进而使其变成引用传递,类似于c++中使用指针。
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        List list = new ArrayList();
        int[] res = new int[nums.length];
        int index = 0;
        int rep = 0;
        int[] ini = new int[2];
        boolean[] flag = new boolean[1];
        flag[0] = false;
        for(int i=0;i<nums.length;i++){
            if(list.size()<k){
                list.add(nums[i]);
            }
            else{
                ini = cal(list,flag,ini);
                res[index++] = ini[0];
                if(ini[1]==0){
                    flag[0] = false;
                }
                if(nums[i]>ini[0]){
                    ini[0] = nums[i];
                    ini[1] = k-1;
                }
                list.remove((int)0);
                ini[1]--;
                list.add(nums[i]);
            }
        }
        ini = cal(list,flag,ini);
        res[index++] = ini[0];
        int[] temp = new int[index];
        for(int i=0;i<index;i++){
            temp[i] = res[i];
        }
        return temp;
    }

    public int[] cal(List list,boolean[] flag,int[] res){
        if(flag[0]){
            return res;
        }
        res[0] = -20000;
        for(int i=0;i<list.size();i++){
            int temp = res[0];
            res[0] = Math.max(res[0] , (int)(list.get((int)i)));
            if(res[0]!=temp){
                res[1] = i;
            }
        }
        flag[0] = !flag[0];
        return res;
    }
}

在这里插入图片描述

方法三:单调队列

我们设计一个单调队列类作为我们维护的滑窗,注意的是其只是维护一个单调递减的数列,这样的话我们在取队列里的最大值时直接拿头部元素即可。

  • 另外这个队列在添加滑窗尾部元素时不只是简单的添加元素,而是先进行判断,判断要加入的元素是否满足原队列的递减关系,如果满足直接添加即可;如果不满足,则判断一下其和队尾元素的关系,如果其大于队尾元素则将队尾元素干掉,自己加入进去,接着继续和队尾进行判断即可。
  • 此队列在删除滑窗头部元素时,也不是直接删除队头元素,因为此队列的顺序不是严格按照滑窗内元素的顺序来的,因此我们不可直接删除队头,而是判断一下要删除的元素是否是队头元素,是则真正的删除,不是则无需删除。
  • 通过设计这样一个单调队列,我们发现问题其实就迎刃而解,并且由于我们每次移动后由队列直接告诉我们最大值,我们无需自己去通过遍历滑窗来得到最大值,因此此法不会超时。

或许你会疑问此单调队列的poppush的设计原理:

  • 为什么push的时候当遇到要添加的元素大于队尾元素时,要删除队尾元素把自己加进去?

因为之所以维护一个单调递减队列,就是为了保存“某个滑窗的可能最大值”,首先队头元素一定是此时滑窗的最大值,接着如果滑窗移动一次后,队头元素可能还是此滑窗的最大值,但也有可能不是,即其刚好是被删除的情况。假如其不是,那么新队列的队头元素就是此滑窗的最大值。

而这个新队头的来源有两个,即要么为原先的队列二号位,要么是新加入滑窗的元素。即使新加入的元素很小,他也是有可能成为“某个滑窗的可能最大值”,因此不能删除。而当新加入的元素处于单调队列的区间内,之所以要实行干掉操作,是因为单调队列的长度是小于等于滑窗的长度,假设其不干掉小于他的元素,当滑窗移动到他应为滑窗最大值(在后面没别的元素干他的情况下)时,我们会发现此时的他不在队头,而且我们发现保留的前面的那些元素并没起到任何作用,因此前面那些元素我们需要干掉,来保证形成一个纯净的有用的单调队列

这里我们可以将单调队列理解为这么一个过程:

  • 单调队列可以理解为在修仙界的一个宗门,宗门的创始人必定是冷血无情,却又实力雄厚的,因此成为了老大,后来他变的仁慈起来,广收弟子。当弟子们十分强大时,又会冷血无情的将比自己弱的人全部干掉,包括老大,干掉之后又会恢复仁慈的性格,收自己的弟子,自己成为老大。当然老大也会自己老死,那么将由老二接管。单调队列便是这样一个如此循环往复的过程。
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int[] res = new int[nums.length];
        int index = 0;
        Myquene myquene = new Myquene();
        for(int i=0;i<k;i++){
            myquene.push(nums[i]);
        }

        for(int i=k;i<nums.length;i++){
            res[index++] = myquene.front();
            myquene.push(nums[i]);
            myquene.pop(nums[i-k]);   
        }

        res[index++] = myquene.front();
        int[] temp = new int[index];
        for(int i=0;i<index;i++){
            temp[i] = res[i];
        }
        return temp;
    }

}

class Myquene{
    List<Integer> list = new ArrayList<>();

    int front(){
        return list.get((int)0);
    }

    void pop(int num){
        if(list.size()!=0 && num==front()){
            list.remove((int)0);
        }
    }

    void push(int num){
        while(list.size()!=0 && num>list.get(list.size()-1)){
            list.remove((int)(list.size()-1));
        }
        list.add(num);
        return;
    }
}

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 好的,我来用中文回复这个链接:https://leetcode-cn.com/tag/dynamic-programming/ 这个链接是 LeetCode 上关于动态规划的题目集合。动态规划是一种常用的算法思想,可以用来解决很多实际问题,比如最长公共子序列、背包问题、最短路径等等。在 LeetCode 上,动态规划也是一个非常重要的题型,很多题目都需要用到动态规划的思想来解决。 这个链接里包含了很多关于动态规划的题目,按照难度从简单到困难排列。每个题目都有详细的题目描述、输入输出样例、题目解析和代码实现等内容,非常适合想要学习动态规划算法的人来练习和提高自己的能力。 总之,这个链接是一个非常好的学习动态规划算法的资源,建议大家多多利用。 ### 回答2: 动态规划是一种算法思想,通常用于优化具有重叠子问题和最优子结构性质的问题。由于其成熟的数学理论和强大的实用效果,动态规划在计算机科学、数学、经济学、管理学等领域均有重要应用。 在计算机科学领域,动态规划常用于解决最优化问题,如背包问题、图像处理、语音识别、自然语言处理等。同时,在计算机网络和分布式系统中,动态规划也广泛应用于各种优化算法中,如链路优化、路由算法、网络流量控制等。 对于算法领域的程序员而言,动态规划是一种必要的技能和知识点。在LeetCode这样的程序员平台上,题目分类和标签设置十分细致和方便,方便程序员查找并深入学习不同类型的算法LeetCode的动态规划标签下的题目涵盖了各种难度级别和场景的问题。从简单的斐波那契数列、迷宫问题到可以用于实际应用的背包问题、最长公共子序列等,难度不断递进且话题丰富,有助于开发人员掌握动态规划的实际应用技能和抽象思维模式。 因此,深入LeetCode动态规划分类下的题目学习和练习,对于程序员的职业发展和技能提升有着重要的意义。 ### 回答3: 动态规划是一种常见的算法思想,它通过将问题拆分成子问题的方式进行求解。在LeetCode中,动态规划标签涵盖了众多经典和优美的算法问题,例如斐波那契数列、矩阵链乘法、背包问题等。 动态规划的核心思想是“记忆化搜索”,即将中间状态保存下来,避免重复计算。通常情况下,我们会使用一张二维表来记录状态转移过程中的中间值,例如动态规划求解斐波那契数列问题时,就可以定义一个二维数组f[i][j],代表第i项斐波那契数列中,第j个元素的值。 在LeetCode中,动态规划标签下有众多难度不同的问题。例如,经典的“爬楼梯”问题,要求我们计算到n级楼梯的方案数。这个问题的解法非常简单,只需要维护一个长度为n的数组,记录到达每一级楼梯的方案数即可。类似的问题还有“零钱兑换”、“乘积最大子数组”、“通配符匹配”等,它们都采用了类似的动态规划思想,通过拆分问题、保存中间状态来求解问题。 需要注意的是,动态规划算法并不是万能的,它虽然可以处理众多经典问题,但在某些场景下并不适用。例如,某些问题的状态转移过程比较复杂,或者状态转移方程中存在多个参数,这些情况下使用动态规划算法可能会变得比较麻烦。此外,动态规划算法也存在一些常见误区,例如错用贪心思想、未考虑边界情况等。 总之,掌握动态规划算法对于LeetCode的学习和解题都非常重要。除了刷题以外,我们还可以通过阅读经典的动态规划书籍,例如《算法竞赛进阶指南》、《算法与数据结构基础》等,来深入理解这种算法思想。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

向光.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值