代码随想录算法训练营第十三天 | 239. 滑动窗口的最大值,347. 前K个高频元素,栈与队列总结

239. 滑动窗口的最大值

之前讲的都是栈的应用,这次该是队列的应用了。

本题算比较有难度的,需要自己去构造单调队列,建议先看视频来理解。 

题目链接/文章讲解/视频讲解:代码随想录

重点:

1. 单调队列

2. 单调队列和优先级队列是有区别的,单调队列如果插入的元素不符合队列的单调性,那么前面的元素都将被删除,而优先级队列是会把这个要插入的元素放到他应该在的位置。

思路:

1. 自定义一个队列

2. 每当新加入一个元素时,判断它是否比前面的元素大,如果是的话,把前面比他小的元素全部pop出队列

3. 窗口往后滑动时,需要判断当前队列口的元素是否为窗口剔出的元素

public static int[] maxSlidingWindow(int[] nums, int k) {
        if (nums.length == 1) {
            return nums;
        }
        int[] result = new int[nums.length - k + 1];
        int resultIndex = 0;
        MyQueue myQueue = new MyQueue();
        // 先把前k个给放入队列
        for (int i = 0; i < k; i++) {
            myQueue.push(nums[i]);
        }
        result[resultIndex++] = myQueue.getMaxValue();
        for (int i = k; i < nums.length; i++) {
            // 窗口滑动了,判断是否需要移除最前面的元素
            myQueue.pop(nums[i - k]);
            // 推入新加的元素
            myQueue.push(nums[i]);
            // 记录最大值
            result[resultIndex++] = myQueue.getMaxValue();
        }
        return result;
}

class MyQueue {
    Deque<Integer> deque;

    public MyQueue() {
        this.deque = new ArrayDeque<>();
    }

    public void pop(int val) {
        // 窗口往后滑动了
        // 比较窗口滑动要弹出的值是否等于队列的第一个值,是的话弹出
        if (!deque.isEmpty() && val == deque.peekFirst()) {
            deque.pollFirst();
        }
    }

    public void push(int val) {
        // 添加元素时,比这个元素小的之前加入的所有值都要弹出
        // val不能大于等于deque.peekLast(),因为pop是把窗口外的元素pop,
        // 如果窗口往后滑,但此时窗口内有两个最大值,如果只保留了一个,
        // 那么本身在后面的最大值也被删除了
        while (!deque.isEmpty() && val > deque.peekLast()) {
            deque.pollLast();
        }
        deque.add(val);
    }

    public int getMaxValue() {
        // 单调递减队列的队列头始终为最大值
        return deque.getFirst();
    }
}

347. 前K个高频元素

大/小顶堆的应用, 在C++中就是优先级队列 

本题是大数据中取前k值的经典思路,了解想法之后,不算难。

题目链接/文章讲解/视频讲解:代码随想录

重点:

1. 优先级队列,大小顶堆的应用

思路:

1. 先用Map将频率记录下来,key:count

2. 循环Map的entrySet,用小顶堆对频率排序,小顶堆只用维护k个

3. 因为是小顶堆,直接倒序输出即可

public static int[] topKFrequent(int[] nums, int k) {
        if (nums.length == 1) {
            return nums;
        }
        // 用Map将频率记录下来
        Map<Integer, Integer> frequentMap = new HashMap<>();
        for (int num : nums) {
            frequentMap.put(num, frequentMap.getOrDefault(num, 0) + 1);
        }
        // 创建一个从小到大存储的优先级队列
        PriorityQueue<int[]> priorityQueue = new PriorityQueue<>((pair1, pair2) -> pair1[1] - pair2[1]);
        for (Map.Entry<Integer, Integer> entry : frequentMap.entrySet()) {
            int[] entrySet = new int[2];
            entrySet[0] = entry.getKey();
            entrySet[1] = entry.getValue();
            // 将数组存入优先级队列,优先级队列将自动按照从小到大排序,小的在队头
            priorityQueue.add(entrySet);
            // 因为优先级队列只需要维护k个,将多余的弹出
            if (priorityQueue.size() > k) {
                priorityQueue.poll();
            }
        }
        // 因为我们的优先级队列是从小到大排序的,所以倒着放入result数组即可
        int[] result = new int[k];
        for (int i = k - 1; i >= 0; i--) {
            result[i] = priorityQueue.poll()[0];
        }
        return result;
}

栈与队列总结

栈与队列的理论基础

  1. 容器类型:在Java中,StackQueue也是容器,但值得注意的是,Java的Queue是一个接口,而不是一个具体的类。

  2. Java版本StackQueue在Java的早期版本中就存在,一直保留至今。

  3. 实现方式:在Java中,Stack是一个具体类,基于动态数组或向量(Vector)来实现,但Vector本身也已经有些过时。Queue是一个接口,它定义了队列的基本行为,然后有不同的实现类可以选择,如LinkedListPriorityQueueArrayDeque等,来实现不同类型的队列。

  4. 迭代器:在Java中,Stack类提供了一个iterator方法,返回一个迭代器,可以用于遍历栈中的元素。但是需要注意,由于栈的特性,遍历栈的迭代器可能会以一种与栈的先进后出(LIFO)顺序不同的方式返回元素。至于Queue接口,它同样提供了迭代器(Iterator)来遍历队列中的元素。根据不同的队列实现,遍历的顺序可能会有所不同,例如LinkedList的队列将遵循先进先出(FIFO)顺序,而PriorityQueue将遵循其自定义的优先级顺序。

了解了栈与队列基础之后,那么可以用栈与队列:栈实现队列 (opens new window)和 栈与队列:队列实现栈 (opens new window)来练习一下栈与队列的基本操作。

值得一提的是,用栈与队列:用队列实现栈还有点别扭 (opens new window)中,其实只用一个队列就够了。

一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时在去弹出元素就是栈的顺序了。

栈经典题目

括号匹配问题

栈与队列:系统中处处都是栈的应用 (opens new window)中我们讲解了括号匹配问题。

括号匹配是使用栈解决的经典问题。

有三种不匹配的情况:

  1. 第一种情况,字符串里左方向的括号多余了 ,所以不匹配。
  2. 第二种情况,括号没有多余,但是 括号的类型没有匹配上。
  3. 第三种情况,字符串里右方向的括号多余了,所以不匹配。

这里还有一些技巧,在匹配左括号的时候,右括号先入栈,就只需要比较当前元素和栈顶相不相等就可以了,比左括号先入栈代码实现要简单的多了!

字符串去重问题

栈与队列:匹配问题都是栈的强项 (opens new window)中讲解了字符串去重问题。

思路就是可以把字符串顺序放到一个栈中,然后如果相同的话 栈就弹出,这样最后栈里剩下的元素都是相邻不相同的元素了。

逆波兰表达式问题

栈与队列:有没有想过计算机是如何处理表达式的? (opens new window)中讲解了求逆波兰表达式。

本题中每一个子表达式要得出一个结果,然后拿这个结果再进行运算,那么这岂不就是一个相邻字符串消除的过程,和栈与队列:匹配问题都是栈的强项 (opens new window)中的对对碰游戏是不是就非常像了。

队列经典题目

滑动窗口最大值问题

栈与队列:滑动窗口里求最大值引出一个重要数据结构 (opens new window)中讲解了一种数据结构:单调队列。

主要思想是队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的。

那么这个维护元素单调递减的队列就叫做单调队列,即单调递减或单调递增的队列。

而且不要以为实现的单调队列就是对窗口里面的数进行排序,如果排序的话,那和优先级队列又有什么区别了呢。

设计单调队列的时候,pop,和push操作要保持如下规则:

  1. pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作
  2. push(value):如果push的元素value大于入口元素的数值,那么就将队列出口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止

保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。

单调队列不是一成不变的,而是不同场景不同写法,总之要保证队列里单调递减或递增的原则,所以叫做单调队列。

不要以为本地中的单调队列实现就是固定的写法。

求前K个高频元素

栈与队列:求前 K 个高频元素和队列有啥关系? (opens new window)中讲解了求前 K 个高频元素。

通过求前 K 个高频元素,引出另一种队列就是优先级队列

什么是优先级队列呢?

其实就是一个披着队列外衣的堆,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。

而且优先级队列内部元素是自动依照元素的权值排列。

那么它是如何有序排列的呢?

缺省情况下priority_queue利用max-heap(大顶堆)完成对元素的排序,这个大顶堆是以vector为表现形式的complete binary tree(完全二叉树)。

什么是堆呢?

堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。

所以大家经常说的大顶堆(堆头是最大元素),小顶堆(堆头是最小元素),如果懒得自己实现的话,就直接用priority_queue(优先级队列)就可以了,底层实现都是一样的,从小到大排就是小顶堆,从大到小排就是大顶堆。

本题就要使用优先级队列来对部分频率进行排序。 注意这里是对部分数据进行排序而不需要对所有数据排序!

栈与队列总结

在栈与队列系列中,我们强调栈与队列的基础,也是很多同学容易忽视的点。

使用抽象程度越高的语言,越容易忽视其底层实现,而C++相对来说是比较接近底层的语言。

我们用栈实现队列,用队列实现栈来掌握的栈与队列的基本操作。

接着,通过括号匹配问题、字符串去重问题、逆波兰表达式问题来系统讲解了栈在系统中的应用,以及使用技巧。

通过求滑动窗口最大值,以及前K个高频元素介绍了两种队列:单调队列和优先级队列,这是特殊场景解决问题的利器,是一定要掌握的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
第二十二天的算法训练主要涵盖了Leetcode题目中的三道题目,分别是Leetcode 28 "Find the Index of the First Occurrence in a String",Leetcode 977 "有序数组的平方",和Leetcode 209 "长度最小的子数组"。 首先是Leetcode 28题,题目要求在给定的字符串中找到第一个出现的字符的索引。思路是使用双指针来遍历字符串,一个指向字符串的开头,另一个指向字符串的结尾。通过比较两个指针所指向的字符是否相等来判断是否找到了第一个出现的字符。具体实现的代码如下: ```python def findIndex(self, s: str) -> int: left = 0 right = len(s) - 1 while left <= right: if s[left == s[right]: return left left += 1 right -= 1 return -1 ``` 接下来是Leetcode 977题,题目要求对给定的有序数组中的元素进行平方,并按照非递减的顺序返回结果。这里由于数组已经是有序的,所以可以使用双指针的方法来解决问题。一个指针指向数组的开头,另一个指针指向数组的末尾。通过比较两个指针所指向的元素的绝对的大小来确定哪个元素的平方应该放在结果数组的末尾。具体实现的代码如下: ```python def sortedSquares(self, nums: List[int]) -> List[int]: left = 0 right = len(nums) - 1 ans = [] while left <= right: if abs(nums[left]) >= abs(nums[right]): ans.append(nums[left ** 2) left += 1 else: ans.append(nums[right ** 2) right -= 1 return ans[::-1] ``` 最后是Leetcode 209题,题目要求在给定的数组中找到长度最小的子数组,使得子数组的和大于等于给定的目标。这里可以使用滑动窗口的方法来解决问题。使用两个指针来表示滑动窗口的左边界和右边界,通过移动指针来调整滑动窗口的大小,使得滑动窗口中的元素的和满足题目要求。具体实现的代码如下: ```python def minSubArrayLen(self, target: int, nums: List[int]) -> int: left = 0 right = 0 ans = float('inf') total = 0 while right < len(nums): total += nums[right] while total >= target: ans = min(ans, right - left + 1) total -= nums[left] left += 1 right += 1 return ans if ans != float('inf') else 0 ``` 以上就是第二十二天的算法训练的内容。通过这些题目的练习,可以提升对双指针和滑动窗口算法的理解和应用能力。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值