算法训练营第十三天| 239. 滑动窗口最大值、 347.前 K 个高频元素 (一刷至少需要理解思路)

83 阅读3分钟

239. 滑动窗口最大值

给你一个整数数组 nums,有一个大小为 k **的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回 滑动窗口中的最大值image.png

思路

使用单调队列

单调队列 顾名思义,单调队列就是在队列的基础上,维护一个单调的序列。

性质:

  1. 队列中的元素其对应在原来的序列中的顺序必须是单调递增的。
  2. 队列中元素的大小必须是单调递(增/减/自定义)。

这道题使用 单调队列 可以很轻松地完成,然而 Java 中并没有单调栈的实现,因此我们需要自己实现一个单调栈的数据结构。

为了实现 单调队列(递增) ,我们可以使用Deque作为基础,并重写 add, poll 以及 peek 函数。

add 函数: 当需要将一个数加入队列之前,我们要确保在该数之前的所有元素都比他大,任何比我们要加入元素小的元素都需要被弹出,这样才能保证栈的单调递增。

如下图这个例子,4是需要加入队列的元素,因此他需要依次和 1 2 3 进行比较,因为 1 2 3 都比 4 小,所以 1 2 3 会被依次弹出,最后队列内只剩下 5 4

image.png

poll 函数
弹出之前检查是否和传入的元素一致,是的话弹出,否则不弹出。 之所以有这道检查是为了保证我们题目中滑动窗口的长度问题。

peek 函数
直接返回 dequepeek 函数。

代码实现

// 自定义单调递增队列
class MyQueue {
    Deque<Integer> deque = new LinkedList<>();

    //弹出元素时,比较当前要弹出的数值是否等于队列出口的数值,如果相等则弹出
    //同时判断队列当前是否为空
    void poll(int val) {
        if (!deque.isEmpty() && deque.peek() == val) {
            deque.pop();
        }
    }

    //添加元素时,如果要添加的元素大于入口处的元素,就将入口元素弹出
    //保证队列元素单调递减
    //比如此时队列元素3,1,2将要入队,比1大,所以1弹出,此时队列:3,2
    void add(int val) {
        while (!deque.isEmpty() && deque.peekLast() < val) {
            deque.pollLast();
        }
        deque.offer(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 idx = 0;
        //自定义队列
        MyQueue queue = new MyQueue();
        //先将前k的元素放入队列
        for (int i = 0; i < k; ++i) {
            queue.add(nums[i]);
        }
        res[idx++] = queue.peek();

        for (int i = k; i < nums.length; ++i) {
            //滑动窗口移除最前面的元素,移除是判断该元素是否放入队列
            queue.poll(nums[i - k]);
            //滑动窗口加入最后面的元素
            queue.add(nums[i]);
            //记录对应的最大值
            res[idx++] = queue.peek();
        }
        return res;
    }
}
  • 时间复杂度: O(n)
  • 空间复杂度: O(k)

参考资料

代码随想录:239. 滑动窗口最大值

347. 前 K 个高频元素

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

image.png

提示:

  • 1 <= nums.length <= 105
  • k 的取值范围是 [1, 数组中不相同的元素的个数]
  • 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的

 

进阶: 你所设计算法的时间复杂度 必须 优于 O(n log n) ,其中 n **是数组大小。

思路

第一眼的思路就是用map统计每个数出现的次数,并且对次数进行排序,取出排名 k 的数组。但是这题要求使用优于 O(n log n) 的算法,而 java 中 Arrays.sort() 函数使用的是快排算法,时间复杂度为 O(n log n),因此该思路行不通。

因此改用大顶堆和小顶堆来实现。 具体思路为先用map统计每个数出现的次数,然后遍历 map 集合,将每个元素按依次加入小(大)顶堆。当遍历完map集合中所有元素之后,我们的 中就是出现频率前 k 高的元素了。然后依次取出放入列表即可。

在这里我们要使用 小顶堆 ,因为我们只维护一个大小为 k 的堆,因此如果是 大顶堆, 我们每次 pop 都会将最大的元素弹出去,这样当遍历完所有元素,最后堆内保存的就是 出现频率前k低的元素。

时间复杂度问题: 因为我们只维护了一个大小为 k 的小顶堆,因此排序的复杂度为 O(nlogk)

代码实现

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

        // 对频率排序
        // 定义一个小顶堆,大小为k, 通过自定义传入的 Comparator 比较器来定义是大顶堆还是小顶堆。
        PriorityQueue<int[]> queue = new PriorityQueue<>((pair1, pair2) -> pair1[1] - pair2[1]);

        // 用固定大小为k的小顶堆,扫面所有频率的数值
        for (Map.Entry<Integer, Integer> entry: map.entrySet()) {
            if (queue.size() < k) {
                queue.add(new int[]{entry.getKey(),entry.getValue()});
            } else {
                if (entry.getValue() > queue.peek()[1]) {
                    queue.poll();
                    queue.add(new int[] {entry.getKey(), entry.getValue()});
                }
            }
        }

        int[] ans = new int[k];
        for(int i = 0 ;i < k;i++){//依次弹出小顶堆,先弹出的是堆的根,出现次数少,后面弹出的出现次数多
            ans[i] = queue.poll()[0];
        }
        return ans;
    }
}
  • 时间复杂度: O(nlogk)
  • 空间复杂度: O(n)

拓展

在写 Comparator 比较器时,return left > right 时是从大到小,return left < right 就是从小到大。

优先级队列的定义正好反过来了,可能和优先级队列的源码实现有关(我没有仔细研究),我估计是底层实现上优先队列队首指向后面,队尾指向最前面的缘故!

此段摘自:代码随想录

参考资料

代码随想录:347.前 K 个高频元素