代码随想录算法训练营第13天(栈和队列3+总结篇 239. 滑动窗口最大值 | 347.前 K 个高频元素

栈与队列part03

  • 239. 滑动窗口最大值 (一刷至少需要理解思路)
      • 难点
      • 不熟悉的语法知识
  • 347.前 K 个高频元素 (一刷至少需要理解思路)
      • 难点
      • 思路
      • 不熟悉的语法知识
  • 总结

239. 滑动窗口最大值 (一刷至少需要理解思路)

之前讲的都是栈的应用,这次该是队列的应用了。
本题算比较有难度的,需要自己去构造单调队列,建议先看视频来理解。
题目链接:239. 滑动窗口最大值
文章讲解/视频讲解:239. 滑动窗口最大值

难点

主要思想:队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的。
双端队列的实现:LinkedList以及ArrayDeque以及常用方法不熟悉
一般ArrayDeque实现队列效率高一些,但是这道题频繁进行插入删除操作,个人觉得用LinkedList更合适一些?!
单调队列的维护

不熟悉的语法知识

Deque中直接子类有两个:LinkedList以及ArrayDeque
ArrayDeque是无初始容量的双端队列,LinkedList则是双向链表

在Deque中,获取并移除元素的方法有两个,分别是removeXxx以及peekXxx。
存在元素时,两者的处理都是一样的。
但是当Deque内为空时,removeXxx会直接抛出NoSuchElementException,
而peekXxx则会返回null。
所以无论在实际开发或者算法时,推荐使用peekXxx方法。
其实ArrayDeque和LinkedList都可以作为栈以及队列使用,但是从执行效率来说,ArrayDeque作为队列,以及LinkedList作为栈使用,会是更好的选择。

注意:
ArrayDeque 是 Deque 接口的一种具体实现,是依赖于可变数组来实现的。ArrayDeque 没有容量限制,可根据需求自动进行扩容。ArrayDeque 可以作为栈来使用,效率要高于 Stack。ArrayDeque 也可以作为队列来使用,效率相较于基于双向链表的 LinkedList 也要更好一些。注意,ArrayDeque 不支持为 null 的元素。

在数据结构上,LinkedList 不仅实现了与 ArrayList 相同的 List 接口,还实现了 Deque 接口

当作为队列使用时,我们会将它与LinkedList 类来做对比。因为 Deque接口继承自 Queue接口,在这里,分别列出两者接口所定义的方法,两者内容区别如下:

参考文章
Java中的queue和deque、ArrayDeque
Java集合常用方法及总结

// b站帅地的代码  美团,腾讯,字节常考题
// 用队列和指针实现
// 时间复杂度:O(n),其中 n 是数组 nums 的长度。每一个下标恰好被放入队列遍历一次,并且最多被弹出队列一次,因此时间复杂度为 O(n)
// 空间复杂度:O(k)。我们使用的辅助队列这个数据结构是双向的,因此「不断从队首弹出元素」保证了队列中最多不会有超过k+1 个元素,因此队列使用的空间为 O(k)
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        if(nums == null || nums.length <= 1){
            return nums;
        }
        ArrayDeque<Integer> queue = new ArrayDeque<>();
        // LinkedList queue = new LinkedList<>(); // arrayList, LinkedList, queue都能实现队列,要找一种自己熟悉的掌握
        int[] res = new int[nums.length - k + 1];
        int index = 0;

        for(int i = 0; i < nums.length; i++){
            // 判断队尾元素是否比新加进来的元素要小  queue.peekLast()队尾元素
            while(!queue.isEmpty() && nums[queue.peekLast()] <= nums[i]){
                queue.pollLast(); // 队尾元素出队!

            }
            queue.add(i); // 等价addLast,在队尾加入元素

            // 去除滑动窗口外的元素
            if(queue.peekLast() - k == queue.peek()){
                queue.poll();  // 删除队首元素
            }
            // 开始记录结果
            if(i + 1 >= k) 
                res[index++] = nums[queue.peek()]; // 队头永远是保存滑动窗口最大值
        }
        return res;
    }
}

347.前 K 个高频元素 (一刷至少需要理解思路)

大/小顶堆的应用, 在C++中就是优先级队列
本题是 大数据中取前k值 的经典思路,了解想法之后,不算难。
题目链接:347.前 K 个高频元素
文章讲解/视频讲解:347.前 K 个高频元素

难点

如何求每个元素出现的频率
如何对频率进行排序并得到出现频率前k高的元素

思路

我们要用小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。
小顶堆实现
先用哈希表统计每个元素出现次数
再用小顶堆实现只得到出现频率前k高的元素:维护一个里面只有k个元素的小顶堆
步骤如下:

  1. 定义哈希表并统计每个元素出现次数
  2. 定义小顶堆并重写compare方法(通过value比较)
  3. 遍历哈希表,维护一个里面只有k个元素的小顶堆(堆中存放的只有key,比较的时候使用的是value)
    (1)小顶堆元素个数 < k-----------------------直接加入
    (2)小顶堆元素个数 >= k && 当前所遍历元素的出现频率>堆顶元素-----------------------poll堆顶元素再入堆当前元素
  4. 定义一个数组,将小顶堆的元素加入到数组中
// 自己代码 也是哈希表+堆
class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        Map<Integer, Integer> map = new HashMap<>();
        for(int i = 0; i < nums.length; i++){
           map.put(nums[i], map.getOrDefault(nums[i], 0) + 1); 
        }

        // int[] 的第一个元素代表数组的值,第二个元素代表了该值出现的次数
        // 优先队列建立小顶堆 方式1
        // PriorityQueue pq = new PriorityQueue<>((o1, o2) ->map.get(o1) - map.get(o2));

        // 方式2
        // 遍历map,用最小堆保存频率最大的k个元素  这样写比方式1快很多
        PriorityQueue<Integer> pq = new PriorityQueue<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer a, Integer b) {
                return map.get(a) - map.get(b);
            }
        });


        for (Integer key : map.keySet()) { //小顶堆只需要维持k个元素有序
            if (pq.size() < k) { //小顶堆元素个数小于k个时直接加
                pq.add(key);
            } else if(map.get(key) > map.get(pq.peek())){
                               //当前元素出现次数大于小顶堆的根结点(这k个元素中出现次数最少的那个)
                pq.poll(); //弹出队头(小顶堆的根结点),即把堆里出现次数最少的那个删除,留下的就是出现次数多的了
                pq.add(key);              
            }
        }
        int[] ans = new int[k];
        for (int i = k - 1; i >= 0; i--) { //依次弹出小顶堆,先弹出的是堆的根,出现次数少,后面弹出的出现次数多
            ans[i] = pq.poll();
        }
        return ans;        

    }
}

不熟悉的语法知识

  1. 哈希表的操作:map.put(nums[i], map.getOrDefault(nums[i], 0) + 1);
  2. 优先队列实现大小顶堆和比较方法的重写
  3. 堆的peek() poll() size() add()方法

总结

栈与队列总结篇

栈与队列的基本操作:栈实现队列用队列实现栈

栈在系统中的应用:括号匹配问题字符串去重问题逆波兰表达式问题

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

你可能感兴趣的:(算法,java,数据结构)