滑动窗口最大值、大小顶堆、前k个高频元素

11 阅读4分钟

239. 滑动窗口最大值

 这题比较难,我们需要找到窗口内的最大值,并写到数组中返回。这题我们用单调队列来写。代码如下:

class Solution {
    Deque<Integer> que=new LinkedList<>();
    public int[] maxSlidingWindow(int[] nums, int k) {
            if(nums==null||nums.length==0){
                return new int[0];
            }
            int n=nums.length;
            int[] res=new int[n-k+1];
            for(int i=0;i<k;i++){
                push(nums[i]);
            }
            res[0] = getMaxValue();
            for(int i=k;i<n;i++){
                pop(nums[i-k]);
                push(nums[i]);
                res[i-k+1]=getMaxValue();
            }
            return res;
    }

    public void pop(int val){
        if(!que.isEmpty()&&val==que.peekFirst()){
            que.pollFirst();
        }
    }

    public void push(int val){
        while(!que.isEmpty()&&val>que.peekLast()){
            que.pollLast();
        }
        que.offerLast(val);
    }

    public int getMaxValue(){
        if (que.isEmpty()) {
            return -1;
        }
        return que.peekFirst();
    }
}

        我们使用一个单调递减队列来存放当前窗口中的最大元素,那么我们获得最大值时从该队列的头部获得即可。但为了保证最大的始终在头部,我们的pop和push函数要有一定的限制。

        push函数:当新元素进入时,会不断从队尾删除所有比它小的元素,因为这些元素不可能再成为未来窗口的最大值。清理完成后,把新元素加入队尾。这样就保证了队列从前到后是递减的结构。

        pop函数:只有当滑出的值等于队头元素时,才需要把队头弹出;如果不等,说明该元素早已在之前被更大的数“挤掉”,队列里已经没有它了,因此不需要操作。

        getMaxValue 方法很简单,直接返回队头元素,因为在单调递减队列中,队头始终是当前窗口的最大值。如果队列为空则返回 -1 作为保护。

        整个代码中,首先进行边界判断,如果数组为空直接返回空数组。然后创建结果数组,长度为 n-k+1。接着先把前 k 个元素依次通过 push 加入单调队列,构成第一个窗口,此时队头就是第一个窗口的最大值,记录到 res[0]。之后开始滑动窗口:每向右移动一步,先调用 pop(nums[i-k]) 移除滑出窗口的元素(如果它正好是当前最大值才真正出队),再调用 push(nums[i]) 把新进入窗口的元素加入队列,并更新当前窗口最大值到结果数组。

大顶堆和小顶堆

        大顶堆和小顶堆都是一种特殊的完全二叉树结构,统称为堆。常用于快速获取最大值或最小值。大顶堆要求每个父节点的值都大于等于它的子节点,因此整棵树的最大值一定在根节点;小顶堆则相反,每个父节点都小于等于子节点,所以最小值在根节点。注意,堆并不是整体有序的,它只保证父子之间的大小关系。

        在 Java 中,最常用的是 PriorityQueue。它默认是小顶堆,如果想要大顶堆,需要自定义比较器。例如:

小顶堆(默认):

PriorityQueue minHeap = new PriorityQueue<>();
minHeap.offer(5);
minHeap.offer(2);
minHeap.offer(8);
System.out.println(minHeap.peek()); // 最小值 2

大顶堆(需要比较器):

PriorityQueue maxHeap = new PriorityQueue<>((a, b) -> b - a);
maxHeap.offer(5);
maxHeap.offer(2);
maxHeap.offer(8);
System.out.println(maxHeap.peek()); // 最大值 8

347. 前 K 个高频元素

        简单了解了大顶堆和小顶堆后,这道题就要使用。我们要求前k个高频元素,那就要用到小顶堆,因为我们要求频率最高的k个,删除时是最先删除顶部的即最小的,且需要使用到哈希来存放元素和频率。

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);
        }

        PriorityQueue<Map.Entry<Integer,Integer>> pq=new PriorityQueue<>((a,b)->a.getValue()-b.getValue());

       for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            pq.offer(entry);
            if (pq.size() > k) {
                pq.poll();
            }
        }
        int[] res=new int[k];
        for(int i=k-1;i>=0;i--){
            res[i]=pq.poll().getKey();
        }
        return res;

    }
}

        首先创建一个 HashMap,用来统计数组中每个数字的出现频率。for 循环遍历 nums 数组,对于每个元素 nums[i],通过 map.getOrDefault(nums[i], 0) 先安全获取当前已有次数(如果不存在就返回 0),然后加 1 再放回 map。执行完这一段后,map 中保存的就是“数字 → 出现次数”的映射关系。

        接下来创建了一个 PriorityQueue,并指定比较器为按 value(频率)升序排列。因为 Java 的 PriorityQueue 默认是小顶堆,所以这里构建的是一个“按频率排序的小顶堆”,堆顶元素永远是当前频率最小的那个。

        然后通过 for-each 循环遍历 map.entrySet(),依次拿到每一个键值对 entry(即某个数字及其出现次数),把它放入小顶堆中。每放入一个元素后,如果堆的大小超过 k,就立刻调用 poll() 弹出堆顶(也就是当前频率最小的元素)。这样做的效果是:堆中始终只保留频率最高的 k 个元素,较小频率的会被不断淘汰。

        最后一步是收集结果。此时堆中剩下的正是前 k 个高频元素,但顺序是从小到大(因为是小顶堆)。代码通过一个从后往前的循环不断 poll 堆顶,并把对应的 key(数字本身)放入结果数组 res 中。逆序填充可以让结果更自然地按频率从高到低排列。