【Leetcode】-代码随想录算法训练营Day13|239. 滑动窗口最大值,347.前 K 个高频元素

Leetcode题目-239. Sliding Window Maximum

链接: 239. Sliding Window Maximum

思路

滑动窗口的移动,就带有很明显的先进先出的特点。所以可以想到此题需要用到队列。但是若使用普通的队列,要求窗口内的最大值,必然在每一次窗口移动后,遍历队列中的元素。这样做的话,复杂度是O(n × k)
为了降低复杂度,我们期望构成一个队列,能在队头始终取到当前队列的最大值。大顶栈能满足这一条件,但是大顶栈在此题中又存在一个问题,在移动窗口时,无法正确获得要被移出的元素,所以要考虑构造新的队列形式。
我们希望它:

  • 队列中最大值始终在队头,且队内元素按单调递减排列。
  • 元素进入时,将其与队尾数(当前最小值)比较,如果它比队尾数值大,则当这个元素进入后,队尾元素则没有了成为最大值的潜力(因为只要当前元素进入了,队尾元素作为先进入的元素,一定比当前元素先出去,而它又比当前元素小,只要当前元素在,它就没办法是最大的元素),所以队尾元素可以提前出队了,没有必要参与后续的流程了。
  • 移出元素时,如果这个元素为最大值,则其一定处在队头,正常移除即可;如果其不为最大值,则它在进队时已被提前移出了。

如此构建我们期望的一个单调队列,移动窗口的过程中就是使用这个队列进行add和remove。此时因为每个元素仅进队、出队一次,复杂度为O(n)

代码实现

class Solution {
   class MyQueue {
       Deque<Integer> deque;

       public MyQueue() {
           deque = new LinkedList<>();
       }

       public void add(int x) {
           while (!deque.isEmpty() && deque.peekLast() < x) {
               deque.removeLast();
           }
           deque.addLast(x);
       }

       public void remove(int x) {
           if (!deque.isEmpty() && deque.peekFirst() == x) {
               deque.removeFirst();
           }
       }

       public int peak() {
           return deque.peekFirst();
       }
   }

   public int[] maxSlidingWindow(int[] nums, int k) {
       MyQueue deque = new MyQueue();
       int[] res = new int[nums.length - k + 1];
       int j = 0;

       for (int i = 0; i < k; i++) {
           deque.add(nums[i]);
       }
       res[j++] = deque.peak();

       for (int i = k; i < nums.length; i++) {
           deque.remove(nums[i - k]);
           deque.add(nums[i]);
           res[j++] = deque.peak();
       }
       return res;
   }
}

总结

时间复杂度:O(n)
空间复杂度:O(k),因为构造了大小为k的辅助队列

Leetcode题目-347. Top K Frequent Elements

链接: 347. Top K Frequent Elements

思路

首先因为要统计元素出现的次数,容易想到使用一个map来管理元素值与元素出现的次数。但如何获取map中value最大的k个值,如果直接对map进行排序,即使使用快速排序,其复杂度也是O(nlog⁡n)。而题目中要求了,复杂度必须优于O(nlog⁡n)
这里考虑使用优先级队列来优化map中元素排序的流程。
我们期望达到以下效果:

  • 队列内元素顺序排列,头部为最小的元素,通过不断add新元素和remove队头的最小元素,维护最大的k个值。
  • 队列对元素进行处理的复杂度小于O(nlog⁡n)

根据优先级队列的概念,可知第一条可以满足,对于第二条,需要了解下优先级队列的实现方式。

优先级队列

特点

  • PriorityQueue中放置的元素按自然顺序排列,自己构造的类或无法比较的类需自己实现Comparable 和 Comparator 接口。
  • 不支持null元素。
  • 可以自动扩容。
  • 插入和删除元素的时间复杂度均为 O(log n)
  • PriorityQueue底层使用了堆数据结构

底层实现

优先级队列的底层数据结构是二叉堆,即完全二叉树。PriorityQueue的默认顺序是小的元素为优先级高的元素(小顶堆),则堆中任一节点的值都不小于其父节点的值。堆的底层是数组。如下图。
请添加图片描述

PriorityQueue可以通过传入自定义的比较器来规定排序规则,所以其也可以改造为大顶堆。

add()方法

add元素之后,为了维持原有的堆结构,会进行以下步骤:

  1. 将元素加入数组末尾,即最后一个叶子结点上;
  2. 与父节点比较,若小于父节点,则与父节点交换位置;
  3. 重复2直至满足添加节点大于其父节点。

如图:
请添加图片描述

poll()方法

poll元素之后,为了维持原有的堆结构,会进行以下步骤:

  1. 因为poll的为根节点的元素(数组的第一个),根节点不能直接删除,先用数组最后一个元素代替根节点。
  2. 再按照堆的规则进行调整,若父节点大于子节点,则将父节点与最小的子节点进行交换,直至满足父节点小于两子节点为止。

如图:
请添加图片描述

由以上可知,插入和删除元素的时间复杂度均为 O(log n)
我们使用一个优先级队列,将每次的map键值对(即数据元素及其出现的次数)塞进去,并按出现次数排列,当数量超过k个的时候,将队首元素(当前count最小的键值对)poll出,这样当map遍历完时,优先级队列内的k个元素即为前k个高频元素。将其对应的key值放到一个int数组里返回即可。

代码实现

这里我自定义了一个数据结构作为优先级队列的元素类型,并根据数据类型实现了其比较的规则。

class Solution {
    public class NumWithCount {
        int num;
        int count;

        public int getCount() {
            return count;
        }

        public int getNum() {
            return num;
        }

        public NumWithCount(int num, int count) {
            this.num = num;
            this.count = count;
        }
    }

    public int[] topKFrequent(int[] nums, int k) {
        Map<Integer, Integer> map = new HashMap<>();
        for (int num : nums) {
            map.put(num, map.getOrDefault(num, 0) + 1);
        }

        PriorityQueue<NumWithCount> priorityQueue = new PriorityQueue<>(Comparator.comparingInt(num -> num.count));
        for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            if (priorityQueue.size() < k) {
                priorityQueue.add(new NumWithCount(entry.getKey(), entry.getValue()));
            } else if (entry.getValue() > priorityQueue.peek().getCount()) {
                priorityQueue.poll();
                priorityQueue.add(new NumWithCount(entry.getKey(), entry.getValue()));
            }
        }

        int[] result = new int[k];
        for (int i = 0; i < k; i++) {
            result[i] = priorityQueue.poll().getNum();
        }
        return result;
    }
}

总结

时间复杂度:O(n log k),遍历元素并计数到map里的复杂度是O(n),当每个元素进入优先级队列时,复杂度是O(log k),一共有n个元素需要进出优先级队列,所以复杂度为O(n log k)
空间复杂度:O(n+k)=O(n),因为构造了大小为k的辅助队列和大小为n的map,已知k<n。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值