代码随想录-Day13 | LeetCode239. 滑动窗口最大值、LeetCode347.前 K 个高频元素

文档讲解: 代码随想录
视频讲解: 《代码随想录》算法公开课-跟着Carl学算法

LeetCode239. 滑动窗口最大值

题目链接:239. 滑动窗口最大值

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

思路:不看随想录的分析根本想不到这种巧妙的思路,而且比较难理解,但是捋清楚之后就会感觉这种思路非常精妙。首先探讨下该题为什么用队列来解决?实际上我们在维护一个窗口并进行滑动的时候,我们需要丢弃最老的数据然后再添加新数据,这就和我们队列的思想很像:先进先出,最先进队列的数据也是最先出队列。然后说下随想录里的思想:

首先需要自定义数组实现一个单调队列,这个队列始终保持单调递减,为什么这么做?其实核心目的就是想让当前队列的头元素始终保持滑窗的最大值,所以当要知道每次滑窗内的最大值时,只需要读取队列的头元素即可

了解核心思想之后,我们探讨自定义的单调队列,如何设计其offer和poll才能和我们滑窗思想进行结合?需要满足下述条件:

  1. 当push元素时,若当前所要push的元素大于前面的元素(程序中就是拿当前元素和双端队列的尾部元素进行比较),则需要将它前面的元素弹出,直到队列前面元素都要大于或等于当前元素为止,再push进去当前元素,这样我们才能满足单调递减的要求。大家可能和我当时都会有疑问,为啥这么做?凭什么直接将队列小于当前元素无条件弹出?其实我们可以再斟酌下随想录的解释,队列其实没有必要维护窗口里的所有元素!我们要的是每次滑窗内的最大值,那些小值本来就会随着滑窗的滑动所丢弃,并且没什么用,我们无条件弹出小于当前元素不会带来什么坏的影响,反而会给我们带来更大的方便,哪里方便? 队列的头元素始终维护的是最大值,我们在读取滑窗最大值时直接从队头读就可以了啊,难道这不方便吗?
  2. 在pop元素时,如果当前要pop的元素不等于当前队列的头元素,则不需要任何处理,为什么?因为这个元素在我们push的时候已经给丢出去了,还处理个毛?所以我们只需要当所pop的元素等于当前队列的头元素时,才执行正常的Pop

完成上述自定义单调队列之后,我们主函数就非常简单了,我们要做的就是始终保持窗口(队列)大小为要求值,在窗口移动时,一旦超出规定的尺寸大小,我们就需要利用自定义的队列pop出一个老数据,随即利用自定义的push进来一个新数据, 然后我们只需要读当前队列头元素即可求每次窗口内部maxNum

import java.util.ArrayDeque;
import java.util.Deque;

class MyQueue {

    Deque<Integer> deque = new ArrayDeque<>();; // 双端队列

    // push函数
    public void offer(int value) {
        // 当value大于前面元素时(也就是和当前队尾进行比较) 则将前面元素弹出 直到都它小于等于前面元素为止
        while (!deque.isEmpty() && value > deque.peekLast()) {
            deque.pollLast(); // 弹出队尾元素
        }
        // 循环判断结束将当前值push进队列
        deque.offerLast(value);
    }

    // pop函数
    public void poll(int value) {
        // 当所要pop的元素为队头元素时 把队头元素弹出 其余不执行任何操作
        if (!deque.isEmpty() && value == deque.peekFirst()) {
            deque.pollFirst();
        }
    }

    // 读取窗口内部maxNum
    public int peek() {
        return deque.peekFirst();
    }

}

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        // 创建滑窗队列 并始终保持大小为k的滑窗
        MyQueue myQueue = new MyQueue();
        // 创建存储结果的数组
        int[] result = new int[nums.length - k + 1];

        for (int i = 0; i < nums.length; i++) {
            if (i < k) {
                myQueue.offer(nums[i]); // 当push元素个数小于k时 正常push
                if (i == k - 1) {
                    result[i - k + 1] = myQueue.peek();
                }
            } else {
                // 当元素个数大于k 弹出一个再进一个
                myQueue.poll(nums[i - k]);
                myQueue.offer(nums[i]);
                result[i - k + 1] = myQueue.peek();
            }
        }
        // 循环结束后返回数组
        return result;
    }
}

LeetCode347.前 K 个高频元素

题目链接:347.前 K 个高频元素

题目描述:给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

思路:首先这道题主要分为3个任务:一是统计元素频率;二是对元素频率进行排序 ;三是取前K个频率最高的元素输出

  1. 统计元素出现的频率可以采用Map作为数据存储结构,其中key存放元素值value统计元素频率
  2. 可以采用优先级队列(底层是二叉树)对部分元素频率进行排序
  3. 优先级队列(堆)其底层结构是二叉树,当poll元素时是从树的根节点弹出元素,为了保证在更新堆的时候,弹出的元素始终是频率小的元素,从而保留下来的是频率大的元素,因此我们选择小顶堆父节点频率始终小于等于左右子树的频率值)。
import java.util.Map;
import java.util.PriorityQueue;

class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        // 创建Map
        Map<Integer, Integer> map = new java.util.HashMap<>();
        // 循环遍历nums 存到map 并统计出现的频率
        for (int num : nums) {
            // 优化一下一: getOrDefault()获取指定key对应的value 如果找不到 key ,则返回设置的默认值
            map.put(num, map.getOrDefault(num, 0) + 1);
        }

        // 对出现频率进行排序 创建优先级队列(小顶堆)
        // lambda表达式设置优先级队列 o1 - o2 为从小到大
        PriorityQueue<int[]> queue = new PriorityQueue<>((o1, o2) -> o1[1] - o2[1]);
        // 遍历map 根据value进行排序 统计频率最高的元素 始终保持堆大小为k

        for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            // 一旦添加的元素超过k个 则弹出头元素 对应频率小的元素
            // 创建二维数组
            int[] arr = new int[2];
            arr[0] = entry.getKey(); // 元素大小
            arr[1] = entry.getValue(); // 元素频率
            // 直接添加到优先队列中排序
            queue.offer(arr);
            // 当大于k时 需要先进排序 再弹出
            if (queue.size() > k) {
                queue.poll();
            }
        }
        // 循环结束后 当前优先级队列只保存前k个频率最高的元素
        // 当前为小顶堆 大值在下面 小值在上面
        // 将key值形成数组返回
        int[] res = new int[k];
        for (int i = k - 1; i >= 0; i--) {
            res[i] = queue.poll()[0];
        }
        return res;
    }
}

栈和队列总结

栈模型可以想象成一个顶端开口的瓶子,其特性是先进后出,后进先出。从随想录中针对栈的题目,其实不难发现由于这个后进先出的特性,非常适合元素的匹配问题,比如说LeetCode20. 有效的括号:寻找我们左括号相同类型的右括号,我们直接从栈顶元素去对比;还有LeetCode1047. 删除字符串中的所有相邻重复项:我们在遍历字符串的时候,一旦发现当前所遍历的元素和栈顶元素重复,直接从栈顶pop进而达到删除重复项的目的。LeetCode150. 逆波兰表达式求值:这种表达式为后缀表达式,当遍历到运算符时直接从栈顶pop两个元素进行相应的运算。总之还需要后续做题慢慢去体会。

队列其特性为先进先出,后进后出。利用这种特性与解决涉及滑窗思想的问题非常合适,窗口在进行滑动时,丢弃老数据并添加新数据。解决LeetCode239. 滑动窗口最大值:需要自定义一个单调队列,始终保持队列的头元素是最大值,要想明白我们如何来设计push和poll来符合我们滑窗的思想。虽然这道题是目前做的第一个困难等级,但想明白其实还是很简单的。总之,队列还需要进一步学习,包括优先级队列,大顶堆,小顶堆等,后续都需要再深入了解。

根据引用\[1\],可以使用暴力解法来求解滑动窗口最大值。具体的做法是,遍历数组,对于每个窗口,使用一个内部循环来找到窗口中的最大值,并将其存储在结果数组中。时间复杂度为O(n*k),其中n为数组长度,k为窗口大小。 根据引用\[2\],还可以使用队列来求解滑动窗口最大值。具体的做法是,使用一个双端队列来维护一个单调递减的窗口。遍历数组,对于每个元素,首先判断队头是否在滑动窗口范围内,如果不在,则将其从队头移除。然后,将当元素与队尾元素比较,如果当元素大于队尾元素,则将队尾元素移除,直到队列为空或者当元素小于等于队尾元素。最后,将当元素的索引插入队尾。如果滑动窗口元素个数达到了k个,并且始终维持在窗口中,就将队头元素加入答案数组中。时间复杂度为O(n),其中n为数组长度。 综上所述,可以使用暴力解法或者使用队列来求解leetcode滑动窗口最大值。 #### 引用[.reference_title] - *1* *3* [leetcode239. 滑动窗口最大值](https://blog.csdn.net/kkkkuuga/article/details/124829581)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [Leetcode#239. 滑动窗口最大值Java解法)](https://blog.csdn.net/paranior/article/details/114890555)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值