代码随想录算法训练营 DAY 13 | 239.滑动窗口最大值 347.前 K 个高频元素

239.滑动窗口最大值

题目链接:https://leetcode.cn/problems/sliding-window-maximum/description/

视频链接:https://www.bilibili.com/video/BV1XS4y1p7qj/?spm_id_from=333.788&vd_source=80cf8293f27c076730af6c32ceeb2689

讲解链接:https://programmercarl.com/0239.%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E6%9C%80%E5%A4%A7%E5%80%BC.html

思路

  • 暴力法:遍历一遍的,每次从窗口中再找到最大的数值。这样是O(n*k)

能不能用大顶堆(优先队列)来存放窗口里的k个数字?

  • 不行。窗口是移动的,而大顶堆每次只能弹出最大值,不能移除其他数字。这样大顶堆维护的就不是窗口里的数字了。

我们需要一个队列,这个队列放进去窗口里的元素,然后随着窗口的移动,队列也一进一出,每次移动之后,队列告诉我们里面的最大值是什么。—单调队列 这个队列没有必要维护

有三个方法:

push()  --处理右边要新加进来的元素
pop()  --处理左边要遗弃掉的元素
getMax() ---实际就是返回队头元素
每移动一次窗口就调用一次这三个方法,getMax得到的都存储在一个数组里

举例:1 3 -1 -3 5 3 2 1 k=3

第一次:单调队列里没有必要维护3之前 比3还小的元素。(3既比1大,又比1活得久,所以1就没用了)3后面的-1可以放进去。

  • push()的规则?如果push进来的元素 比 处于队头的都要大,前面的都要排除。直到队头元素没有比要push进来的元素大为止。如果比前面的小,放进去就行。新进来的如果比当前单调队列队尾的大,就把前面的全部踢走
  • pop()的规则?如果要pop的元素等于当前窗口里的最大值(队头),就要弹出。
  • getMax() 实际就是返回队头就行。

关键:单调队列里维护的最大值始终是队头(出口处)元素!有潜力成为最大值的才会加入到队列里。)

在这里插入图片描述

  • 队列出口元素:deque.peek()

  • 队列入口元素:deque.getLast()

  • 注意在每次要进行操作前都要先判空,避免空指针异常!

  • 先看是否nums只有一个元素,是就直接返回。然后初始化单调队列,把第一次放进去。

在这里插入图片描述

class MyQueue {
    Deque<Integer> deque = new LinkedList<>();
    //弹出元素时,比较当前要弹出的数值是否等于队列出口的数值,如果相等则弹出
    void poll(int val) {
        if (!deque.isEmpty() && val == deque.peek()) {
            deque.poll();
        }
    }
    //添加元素时,如果要添加的元素大于入口处的元素,就将入口元素弹出
    void add(int val) {
        while (!deque.isEmpty() && val > deque.getLast()) {
            deque.removeLast();
        }
        deque.add(val);
    }

    int peek() {
        return deque.peek();
    }
}

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        if (nums.length == 1) {
            return nums;
        }
        int len = nums.length - k + 1;
        //存放结果元素
        int[] res = new int[len];
        int num = 0;

        //初始化单调队列,先将前k的元素放入队列
        MyQueue myQueue = new MyQueue();
        for (int i = 0; i < k; i++) {
            myQueue.add(nums[i]);
        }
        //第一个窗口里的最大值
        res[num++] = myQueue.peek();
        for (int i = k; i < nums.length; i++) {
            //滑动窗口移除左边的元素
            myQueue.poll(nums[i - k]);
            //滑动窗口加入右侧的元素
            myQueue.add(nums[i]);
            //记录对应的最大值
            res[num++] = myQueue.peek();
        }
        return res;
    }
}

347.前 K 个高频元素

题目链接:https://leetcode.cn/problems/top-k-frequent-elements/

视频链接:https://www.bilibili.com/video/BV1Xg41167Lz/?spm_id_from=333.788&vd_source=80cf8293f27c076730af6c32ceeb2689

讲解链接:https://programmercarl.com/0347.%E5%89%8DK%E4%B8%AA%E9%AB%98%E9%A2%91%E5%85%83%E7%B4%A0.html

思路

考虑三个内容:

  1. 要统计元素出现频率
  2. 对频率排序
  3. 找出前K个高频元素

先遍历一次所有元素,用map集合来统计出现的次数。key=元素,value=出现次数

  • 怎么统计?常用套路:
Map<Integer, Integer> map = new Map<>();
        for(int num : nums) {
            //根据key查询对应value,查不到就返回传的默认值(第二个参数)
            map.put(num,map.getOrDefault(num,0)+1);
        }

我们其实没有必要对所有元素(出现的次数)进行排序!只要维护前k个—大顶堆和小顶堆

  • 大顶堆:根节点是最大的(底层是二叉树)
  • 小顶堆:根节点是最小的

我们只要用它遍历一遍所有元素,维护它的规模是k就行。用哪种堆呢?

  • 第一反应可能是用大顶堆。如果是大顶堆,假如要加进来一个元素,就要弹出一个。而堆这种结构要弹出都是从堆顶弹出的。用大顶堆来遍历,每次弹出的都是堆里当前最大的那个,所以最后剩下的是前k个小的了。
  • 所以我们用小顶堆。每次不断把堆里最小的元素移除

在这里插入图片描述

时间复杂度

如果对所有元素进行排序,时间复杂度是O(n*logn)

在这个k大小的堆里调整元素,时间复杂度是O(logk)。因此总的时间复杂度是O(n*logk)

关于map和优先队列

我们这里的小根堆(优先队列)每个节点是一个长度为2的数组![num,cnt]

pq.peek()就是在堆顶的元素,一个二元组 pq.peek()[1] 就是出现的次数!

  • java中小/大顶堆采用优先队列实现:
默认是小根堆,大根堆需要用户提供比较器:new PriorityQueue<>(Collections.reverseOrder())
比较器可以写成lambda表达式形式。 o2-o1是大根 o1-o2是小根!
  • 如何遍历?
//创建小根堆,传入lambda表达式,每个元素是一个长度为2的数组[num,cnt]
PriorityQueue<int[]> pq = new PriorityQueue<>((pair1,pair2)->pair1[1]-pair2[1]);
for(Map.Entry<Integer,Integer> entry : map.entrySet()){  //用entry来遍历
	 if(pq.size()<k){ //当小顶堆中元素个数小于k个时 直接添加进堆
          pq.add(new int[]{entry.getKey(),entry.getValue()});
     }else{
          if(entry.getValue()>pq.peek()[1]){//当前元素出现次数大于小顶堆的根结点(这k个元素中出现次数最少的那个)
               pq.poll();//弹出队头(小顶堆的根结点),即把堆里出现次数最少的那个删除,留下的就是出现次数多的了
               pq.add(new int[]{entry.getKey(),entry.getValue()});
           }
      }    
}

完整java代码

class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        Map<Integer, Integer> map = new HashMap<>();
        for(int num : nums) {
            //根据key查询对应value,查不到就返回传的默认值(第二个参数)
            map.put(num,map.getOrDefault(num,0)+1);
        }
        //在优先队列中存储二元组(num,cnt),cnt表示元素值num在数组中的出现次数
        //下面这个是lambda表达式 比较的是pair[1]也就是出现次数 o2-o1是大根 o1-o2是小根!
        PriorityQueue<int[]> pq = new PriorityQueue<>((pair1,pair2)->pair1[1]-pair2[1]);
        for(Map.Entry<Integer,Integer> entry : map.entrySet()){ //小顶堆只需要维持k个元素有序
            if(pq.size()<k){//小顶堆元素个数小于k个时直接加
                pq.offer(new int[]{entry.getKey(),entry.getValue()});
            }else{
                if(entry.getValue()>pq.peek()[1]){//当前元素出现次数大于小顶堆的根结点(这k个元素中出现次数最少的那个)
                    pq.poll();//弹出队头(小顶堆的根结点),即把堆里出现次数最少的那个删除,留下的就是出现次数多的了
                    pq.offer(new int[]{entry.getKey(),entry.getValue()});
                }
            }
        }
        int[] ans = new int[k];
        //倒序弹出小顶堆,先弹出的是堆的根,出现次数少,最后弹出的出现次数多
        for(int i=k-1;i>=0;i--){
            ans[i] = pq.poll()[0];
        }
        return ans;
    }
}

栈与队列小总结

  • 比较一下队列和栈相关方法的不同:

    队列:

    add():在队尾添加元素
    remove():删除并返回队头
    isEmpty():是否为空

    栈:

    push():压入元素

    pop():弹出栈顶元素,并返回栈顶元素

    peek():返回栈顶元素

    empty():是否为空

其他求长度size(),求堆头/栈顶peek()和清空clear()都是一样的!

  • 注意在java中,栈是一个实现类。而队列是一个接口,实现类有LinkedList和PriorityQueue

  • 关于deque

队头元素:deque.peek()
队尾元素:deque.getLast()
加入元素:deque.add()
移除元素:deque.poll()
  • 关于map的遍历
    在这里插入图片描述

用.entryset()方法 把Map转换成Set集合

HashMap集合转化成Set集合后,Set集合中每一个元素都是Map.Entry<Integer,String> 这个类型的数据

Map.Entry<K,V> 表示一个映射项

可以通过entry.getValue()entry.getKey()获得对应的value或者key

  • 优先队列的使用:
默认是小根堆,大根堆需要用户提供比较器:new PriorityQueue<>(Collections.reverseOrder())
比较器可以写成lambda表达式形式。 o2-o1是大根 o1-o2是小根!
    
q.size();  //获取元素个数
q.offer();  //插入元素  add()也可以
q.peek();  //获取优先级最高的元素
q.poll();  //移除优先级最高的元素 并返回
q.isEmpty();  //判空
  • 26
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值