算法训练营打卡Day13 | 239. 滑动窗口最大值 347.前 K 个高频元素 总结

一、LC239. 滑动窗口最大值

力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

视频讲解:单调队列正式登场!| LeetCode:239. 滑动窗口最大值_哔哩哔哩_bilibili

暴力方法,遍历一遍的过程中每次从窗口中再找到最大的数值,这样很明显是O(n × 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()就可以返回当前窗口的最大值。

JAVA

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        MyQueue window = new MyQueue();

        int[] result = new int[nums.length - k + 1];

        for (int i = 0; i < k; i++) {
            window.push(nums[i]);
        }

        result[0] = window.getMax();
        for (int i = k; i < nums.length; i++) {
            System.out.println(i-k+1 + " " + nums[i]);
            window.poll(nums[i-k]);
            window.push(nums[i]);
            result[i-k+1] = window.getMax();
            
        } 
        return result;
    }
}

class MyQueue {
    Deque<Integer> window = new ArrayDeque<>();

    //删除元素
    void poll(int val) {
        if (!window.isEmpty() && window.peekFirst() == val) {
            window.pollFirst();
        }
    }

    //添加元素
    void push(int val) {
        while (!window.isEmpty() && window.peekLast() < val) {
            window.pollLast();
        }
        window.offerLast(val);
    }

    int getMax() {
        return window.peekFirst();
    }
}

Python

class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        result = []
        queue = deque()
        for i in range(k):
            while len(queue) > 0 and nums[i] > queue[-1]:
                queue.pop()
            queue.append(nums[i])
        result.append(queue[0])

        for j in range(k, len(nums)):
            remove = nums[j-k]
            if remove == queue[0]:
                queue.popleft()
            
            while len(queue) > 0 and queue[-1] < nums[j]:
                queue.pop()
            queue.append(nums[j])
            result.append(queue[0])
        return result
  • 时间复杂度: O(n)
  • 空间复杂度: O(k)

再来看一下时间复杂度,使用单调队列的时间复杂度是 O(n)。

有的同学可能想了,在队列中 push元素的过程中,还有pop操作呢,感觉不是纯粹的O(n)。

其实,大家可以自己观察一下单调队列的实现,nums 中的每个元素最多也就被 push_back 和 pop_back 各一次,没有任何多余操作,所以整体的复杂度还是 O(n)。

空间复杂度因为我们定义一个辅助队列,所以是O(k)。

二、LC347.前 K 个高频元素

力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

先遍历数组,用map统计每个元素出现的频率。

遍历map,维护一个大小为k的小顶堆。最后得到的结果是从小到大排序的前k个高频元素。翻转得到从大到小排序的前k个高频元素。

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

        PriorityQueue<Integer> pq = new PriorityQueue<>(new Comparator<Integer>() {
            public int compare(Integer a, Integer b){
                return count.get(a) - count.get(b);
            }
        });

        for (Integer key: count.keySet()) {
            if (pq.size() < k) {//小顶堆元素个数小于k个时直接加
                System.out.println(key + " 小于k");
                pq.add(key);
            } else if (count.get(pq.peek()) < count.get(key)) {
                //当前元素出现次数大于小顶堆的根结点(这k个元素中出现次数最少的那个)
                pq.poll();弹出队头(小顶堆的根结点),即把堆里出现次数最少的那个删除,留下的就是出现次数多的了
                pq.add(key);
                System.out.println(key + " 大于k");
            }
        }

        System.out.println(k + " " + pq.size());
        int[] results = new int[k];
        for (int i = 0; i < k; i++) {//依次弹出小顶堆,先弹出的是堆的根,出现次数少,后面弹出的出现次数多
        results[i] = pq.poll();
        }

        return results;
    }
}

三、总结

1. 可以出一道面试题:栈里面的元素在内存中是连续分布的么?

这个问题有两个陷阱:

  • 陷阱1:栈是容器适配器,底层容器使用不同的容器,导致栈内数据在内存中是不是连续分布。
  • 陷阱2:缺省情况下,默认底层容器是deque,那么deque的在内存中的数据分布是什么样的呢? 答案是:不连续的,下文也会提到deque。

代码随想录

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值