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

239. 滑动窗口最大值

题目链接 | 解题思路

暴力的解法自然是取每个区间,分别求最大值,然后得到结果,复杂度为 O ( n k ) O(nk) O(nk),但会超时。

尝试用队列来解决问题,会发现无法获得足够的单调性。普通的队列可以保持 index 的单调,便于窗口的滑动,但无法便捷地获取最大值;有序队列,例如 priority queue (heap)可以保持值的单调,但是无法便捷地删除要 pop 的元素(不知道对应的 index)。本题中似乎同时需要 index 的单调性以及值的单调性,但目前没有现成的结构能有效地同时实现这两大单调性,从概念上来说两种单调性就是冲突的。

实际上,我们并不需要完全维持窗口中的值单调性,而只需要维持最大值,或者说有可能成为最大值的元素,并保持队列中的元素是从大到小的,我们称之为单调队列(需要自己实现)。

单调队列最重要的思想即是维持有可能成为(窗口内)最大值的元素的有序性,而不是整个窗口的有序性。举例来说,对于窗口[2, 3, 5, 1, 4],只有[5, 4]具有被维护的价值,因为当遇到5的时候,即知道前面的2、3都不会成为当前以及之后包括它们的窗口中的最大值。同理,1也不会被维持。然而,4却会被维持,因为假如4的后面全部是小于4的元素,则当窗口继续滑动,可能会出现4成为某一窗口最大值的情况。
同时,这也使得窗口内是单调递减的。

维护有可能成为(窗口内)最大值的元素,也是对于窗口的一种剪枝。

完整的演算过程如下

  • push:当添加一个新元素时,与队列入口(back)的元素比较,若新元素更大,则 pop 入口元素,直到新的元素小于入口元素。此时添加新元素。
  • pop:如果需要 pop 的元素并非当前队列的最大元素(即出口元素,因为队列从大到小排列),则该元素已经在之前某一次 push 中被 pop 了。仅当需要 pop 的元素就是当前队列中的最大元素时,才需要 pop 该元素。
  • getMax:由于维护的队列从大到小,队列的出口元素即为最大的元素。

定义并维持了一个新的队列,同时每个元素至多被 push 和 pop 各一次。

  • 时间复杂度: O ( n ) O(n) O(n)
  • 空间复杂度: O ( k ) O(k) O(k)

因为需要在队列的两端进行操作,pop 开头的元素的时候 list 的复杂度太高了,所以使用 deque 操作。

from collections import deque
class MyMaxQueue:
    def __init__(self):
        self.data = deque()     # low complexity using deque, two-sided list

    def pop(self, item: int) -> None:
        """
        Compare the item to pop and the front (max) item in the list. Only pop when they are the same.
        This means that we will pop the max item in the deque. Otherwise, the item to pop should have already been removed from the deque previously.
        """
        if len(self.data) > 0 and item == self.data[0]:     # always check whether empty first
            self.data.popleft()                             # O(1)
    
    def push(self, item: int) -> None:
        """
        Keep the deque containing elements in a non-decreasing order. 
        If the pushed item is greater than the back of deque, pop the back item until the pushed item is smaller than or equal to the back.
        """
        while (len(self.data) > 0 and item > self.data[-1]):
            self.data.pop()
        self.data.append(item)      

    def getMax(self) -> int:
        return self.data[0]         # non-decreasing order


class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        queue = MyMaxQueue()
        for i in range(k):
            queue.push(nums[i])
        results = [queue.getMax()]
        for i in range(len(nums) - k):
            queue.pop(nums[i])
            queue.push(nums[i + k])
            results.append(queue.getMax())
        return results

347. 前 K 个高频元素

题目链接 | 解题思路

最朴实暴力的思想就是将数组储存成 mapping(key 是数值,value 是出现次数),然后根据出现次数排序,选取前 k 项的 key。非常直接的想法,时间复杂度为 O ( n log ⁡ n ) O(n\log{n}) O(nlogn),主要取决于排序。
但题目要求复杂度优于 O ( n log ⁡ n ) O(n\log{n}) O(nlogn),相当于直接否定了需要整体排序的解法。注意到,对于题目要求中的“前 k 个”元素,实际上并不需要对于整个 mapping 进行排序,而是仅需维持前 k 个的排序即可。

于是我们想到了 heap 的应用。
在本题中,是要维持前 k 个高频元素,似乎要记录的是最大的一些值,自然会想到 max heap。但是限定 heap size 之后,决定是否将新的元素加入 max heap 需要将该元素与 max heap 的最小值比较,彻底违反了 max heap 的初衷。所以,min heap 才是此题的最优解,因为我们添加进 heap 的基准是与 heap 中最小的值进行比较。
具体实现可以采用 priority queue。

Heap

Heap 是一个数组形式的 complete binary tree。以 max heap 为例,其中每一个 parent node 的值都大于等于其 child nodes 的值。显然,heap 的内部是有序的,并且以 binary tree 的形式来维持这个排序。

优先级队列 priority queue

优先级队列一般是由 heap 来实现的,其中每个元素都有自己相对应的 priority,在 python 中,priority 越小代表优先级越高(越早被取出)。优先级队列维护 priority 从小到大的排序。

优先级队列的应用之一是任务处理,保证优先处理高优先级的任务。

from queue import PriorityQueue

queue = PriorityQueue()

q.put((2, 'g'))
q.put((3, 'e'))

q.get()			# 'g', remove and return the item with lowest priority

q.qsize()		# 1

参考1 | 参考2

暴力解法

对整个 mapping 根据出现次数排序,取前 k 个。

from collections import Counter

class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        records = Counter(nums)
        records_lst = []
        for key in records:
            records_lst.append([key, records[key]])
        
        records_lst.sort(key=lambda x:x[1], reverse=True)
        results = []
        for item in records_lst[:k]:
            results.append(item[0])
        return results

Min Heap - queue.PriorityQueue

时间复杂度: O ( n log ⁡ k ) O(n\log{k}) O(nlogk)
对于每个(key,value)的插入,维护 heap 的复杂度为 O ( log ⁡ k ) O(\log{k}) O(logk)

from collections import Counter
from queue import PriorityQueue

class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        # turn nums into a mapping: (num, count)
        records = Counter(nums)

        # set up a priority queue
        queue = PriorityQueue()
        for key in records:
            queue.put((records[key], key))
            if queue.qsize() > k:
                queue.get()
        
        # load the priority queue's elements
        results = []
        while (not queue.empty()):
            results.append(queue.get()[1])
        return results

Min Heap - heapq

heapq 是 python 中实现 heap 的一个辅助库。不像 queue.PriorityQueue 是一个独立的子类,heapq 中的函数可以直接作用于 list,只是将 list 维持在 min heap 的规则下。

常用的 API:
heapq.heappush(heap, item): Push the value item onto the heap, maintaining the heap invariant.

heapq.heappop(heap): Pop and return the smallest item from the heap, maintaining the heap invariant. If the heap is empty, IndexError is raised. To access the smallest item without popping it, use heap[0].

heapq.heappushpop(heap, item): Push item on the heap, then pop and return the smallest item from the heap. The combined action runs more efficiently than heappush() followed by a separate call to heappop().

heapq.heapify(x): Transform list x into a heap, in-place, in linear time.

heapq参考

值得注意的是,heapq 在添加元素时也要遵循 (priority, value) 的格式。

import heapq

class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        # another way to implement Counter
        records = {}
        for i in nums:
            records[i] = records.get(i, 0) + 1

        # initialize a list - min_heap
        min_heap = []
        for key in records:
            heapq.heappush(min_heap, (records[key], key))
            if len(min_heap) > k:
                heapq.heappop(min_heap)
        
        results = []
        for item in min_heap:
            results.append(item[1])
        return results
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
代码随想录算法训练营是一个优质的学习和讨论平台,提供了丰富的算法训练内容和讨论交流机会。在训练营中,学员们可以通过观看视频讲解来学习算法知识,并根据讲解内容进行刷题练习。此外,训练营还提供了刷题建议,例如先看视频、了解自己所使用的编程语言、使用日志等方法来提高刷题效果和语言掌握程度。 训练营中的讨论内容非常丰富,涵盖了各种算法知识点和解题方法。例如,在第14天的训练营中,讲解了二叉树的理论基础、递归遍历、迭代遍历和统一遍历的内容。此外,在讨论中还分享了相关的博客文章和配图,帮助学员更好地理解和掌握二叉树的遍历方法。 训练营还提供了每日的讨论知识点,例如在第15天的讨论中,介绍了层序遍历的方法和使用队列来模拟一层一层遍历的效果。在第16天的讨论中,重点讨论了如何进行调试(debug)的方法,认为掌握调试技巧可以帮助学员更好地解决问题和写出正确的算法代码。 总之,代码随想录算法训练营是一个提供优质学习和讨论环境的平台,可以帮助学员系统地学习算法知识,并提供了丰富的讨论内容和刷题建议来提高算法编程能力。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [代码随想录算法训练营每日精华](https://blog.csdn.net/weixin_38556197/article/details/128462133)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值