代码随想录第十三天|239. 滑动窗口最大值 ● 347.前 K 个高频元素

滑动窗口最大值

题目链接

思路

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

那么这个维护元素单调递减的队列就叫做单调队列,即单调递减或单调递增的队列。C++中没有直接支持单调队列,需要我们自己来实现一个单调队列

对于窗口里的元素{2, 3, 5, 1 ,4},单调队列里只维护{5, 4} 就够了,保持单调队列里单调递减,此时队列出口元素就是窗口里最大元素。

此时大家应该怀疑单调队列里维护着{5, 4} 怎么配合窗口进行滑动呢?

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

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

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

代码

class MyQueue {
    Deque<Integer> deque = new LinkedList<>();
    //弹出元素时,比较当前要弹出的数值是否等于队列出口的数值,如果相等则弹出
    //同时判断队列当前是否为空
    void poll(int val) {
        if (!deque.isEmpty() && val == deque.peek()) {
            deque.poll();
        }
    }
    //添加元素时,如果要添加的元素大于入口处的元素,就将入口元素弹出
    //保证队列元素单调递减
    //比如此时队列元素3,1,2将要入队,比1大,所以1弹出,此时队列:3,2
    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;
        //自定义队列
        MyQueue myQueue = new MyQueue();
        //先将前k的元素放入队列
        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;
    }
}

解释一下Dequeue:

在Java中,QueueDeque是两种不同类型的集合,它们都用于以特定顺序存储元素,但在功能和使用方式上有所不同。

Queue

Queue接口在Java集合框架中定义,代表了一个先进先出(FIFO)的队列。队列通常用于在处理前按照顺序保持元素。Queue接口提供了基本的队列操作方法,如offer(向队列添加元素)、poll(移除并返回队列头部的元素)、peek(返回队列头部的元素但不移除)等。

Deque

Deque(双端队列)接口扩展了Queue接口,提供了更灵活的队列操作,允许元素从队列的两端被添加或移除,即它支持先进先出(FIFO)和后进先出(LIFO)两种模式。Deque接口提供了一系列方法,包括addFirstaddLastofferFirstofferLastremoveFirstremoveLastpollFirstpollLastgetFirstgetLast等,使得它既可以作为标准队列使用,也可以作为栈使用。

主要区别

  • 功能范围Deque接口提供了比Queue接口更广泛的功能,支持从两端插入和移除元素。
  • 使用场景:由于Deque支持更灵活的元素操作,它可以被用于更多场景,如当作栈使用(后进先出)或者标准队列(先进先出)。
  • 方法差异Deque接口包含了Queue接口的所有方法,并且添加了一些额外的方法来支持双端操作。

在选择使用Queue还是Deque时,应考虑所需的数据结构操作类型。如果需要标准的队列操作,Queue就足够了;如果需要从两端操作数据,或者需要一个可以同时支持队列和栈操作的数据结构,那么Deque会是更好的选择。

前k个高频元素

题目链接

思路

这道题目主要涉及到如下三块内容:

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

首先统计元素出现的频率,这一类的问题可以使用map来进行统计。

然后是对频率进行排序,这里我们可以使用一种 容器适配器就是优先级队列

什么是优先级队列呢?

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

而且优先级队列内部元素是自动依照元素的权值排列。那么它是如何有序排列的呢?

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

什么是堆呢?

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

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

本题我们就要使用优先级队列来对部分频率进行排序。

为什么不用快排呢, 使用快排要将map转换为vector的结构,然后对整个数组进行排序, 而这种场景下,我们其实只需要维护k个有序的序列就可以了,所以使用优先级队列是最优的。

此时要思考一下,是使用小顶堆呢,还是大顶堆?

有的同学一想,题目要求前 K 个高频元素,那么果断用大顶堆啊。

那么问题来了,定义一个大小为k的大顶堆,在每次移动更新大顶堆的时候,每次弹出都把最大的元素弹出去了,那么怎么保留下来前K个高频元素呢。

而且使用大顶堆就要把所有元素都进行排序,那能不能只排序k个元素呢?

所以我们要用小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。

寻找前k个最大元素流程如图所示:(图中的频率只有三个,所以正好构成一个大小为3的小顶堆,如果频率更多一些,则用这个小顶堆进行扫描)

347.前K个高频元素

代码

class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        // 优先级队列,为了避免复杂 api 操作,pq 存储数组
        // lambda 表达式设置优先级队列从大到小存储 o1 - o2 为从小到大,o2 - o1 反之
        PriorityQueue<int[]> pq = new PriorityQueue<>((o1, o2) -> o1[1] - o2[1]);
        int[] res = new int[k]; // 答案数组为 k 个元素
        Map<Integer, Integer> map = new HashMap<>(); // 记录元素出现次数
        for(int num : nums) map.put(num, map.getOrDefault(num, 0) + 1);
        for(var x : map.entrySet()) { // entrySet 获取 k-v Set 集合
            // 将 kv 转化成数组
            int[] tmp = new int[2];
            tmp[0] = x.getKey();
            tmp[1] = x.getValue();
            pq.offer(tmp);
            // 下面的代码是根据小根堆实现的,我只保留优先队列的最后的k个,只要超出了k我就将最小的弹出,剩余的k个就是答案
            if(pq.size() > k) {
                pq.poll();
            }
        }
        for(int i = 0; i < k; i ++) {
            res[i] = pq.poll()[0]; // 获取优先队列里的元素
        }
        return res;
    }
}

我们不管是什么元素 先把他都offer进来 再poll他会自动poll最小的 因为我们采取的是优先级队列

这里的var x也可以替换成Map.Entry<Integer,Integer> entry

总结

栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。

栈往往不被归类为容器,而被归类为container adapter(容器适配器)。 默认为deque

在Java中,栈(Stack)和队列(Queue)这两种数据结构的底层实现方式多样,主要依赖于Java集合框架(Java Collections Framework)提供的类和接口。下面是一些常见的实现方式:

栈的实现

Java中的栈可以通过java.util.Stack类实现,也可以使用Deque接口的实现类,如ArrayDeque,来模拟栈的行为。java.util.Stack类本身继承自Vector,因此它的实现是基于数组的,但是由于Stack是一种遗留类,通常推荐使用更现代的ArrayDeque来实现栈的功能。

  • java.util.Stack

    • 底层基于动态数组实现。
    • 提供了标准的栈操作,例如push(入栈)、pop(出栈)、peek(查看栈顶元素)等。
    • 由于继承自Vector,它是同步的(线程安全),但在单线程环境下可能导致不必要的性能开销。
  • java.util.ArrayDeque

    • 底层使用循环数组实现。
    • 可以高效地从两端添加或移除元素,非常适合作为栈使用。
    • 不是线程安全的,但在单线程环境中性能优于Stack
    • 使用pushpoppeek方法来模拟栈操作。

队列的实现

队列在Java中通常是通过java.util.Queue接口实现的,这是一个继承自java.util.Collection的接口。Queue接口有多种实现,包括LinkedListArrayDequePriorityQueue等,每种实现有其特定的用途。

  • java.util.LinkedList

    • 同时实现了ListDeque接口。
    • 底层基于双向链表实现。
    • 适用于需要频繁进行元素插入和删除操作的场景。
  • java.util.ArrayDeque

    • 底层使用循环数组实现。
    • 既可以作为队列使用,也可以作为栈使用。
    • 适用于需要高效随机访问的场景。
  • java.util.PriorityQueue

    • 底层基于堆结构(通常是二叉堆)实现,用于创建优先级队列。
    • 元素按照其自然顺序或者构造时提供的Comparator进行排序。
    • 不保证同等优先级元素的顺序。

总结

Java中的栈和队列可以通过不同的类实现,选择合适的实现取决于具体的应用需求。例如,如果需要线程安全的栈实现,可以选择Stack;如果需要高效的非同步栈实现,可以使用ArrayDeque。对于队列,LinkedList适合频繁的插入删除操作,ArrayDeque适合大量的随机访问操作,而PriorityQueue适用于需要元素排序的场景。

  • 20
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值