代码随想录算法训练营第十三天(栈与队列part03) | 239. 滑动窗口最大值,347.前 K 个高频元素,总结

📝239. 滑动窗口最大值 (Hard)

之前讲的都是栈的应用,这次该是队列的应用了。本题算比较有难度的,需要自己去构造单调队列,建议先看视频来理解。

题目链接/文章讲解/视频讲解:

👩‍💻思路:


1.Hint:

  • 💡提示 1:考虑使用 deque(双端队列)等数据结构。
  • 💡提示 2:队列size不必与窗口大小完全相同。
  • 💡提示 3:删除多余的元素,队列应只存储和维护需要考虑的元素。

2.知识点补充:

  • 为什么我们用deque实现这个单调队列呢?文章中提到,使用deque最为合适,在文章栈与队列:来看看栈和队列不为人知的一面 (opens new window)中,我们就提到了常用的queue在没有指定容器的情况下,deque就是默认底层容器。Deque 和 Queue 的应用场景:Deque 适用于需要在两端都能进行高效操作的场景,如维护一个滑动窗口的最大值或最小值;Queue 适用于需要严格遵循先进先出规则的场景,如任务队列和消息队列。如果需要实现一个单调队列,使用 deque 是非常合适的选择,因为它允许在两端高效地进行添加和删除操作。
  • 单调队列:一种数据结构,通过保持元素的最大值始终位于前端的方式,帮助高效查找滑动窗口中的最大值。
  • 滑动窗口:一种在列表/数组中移动固定大小窗口的技术,用于解决查找每个窗口中的最大值/最小值/平均值等问题。

3.详细讲解:

请切记,在滑动窗口的算法中,每次窗口向前移动一步,我们需要做以下两件事:

  • 加入新的元素:将新的元素加入到窗口中。
  • 移出旧的元素:移除窗口中已经不再属于当前窗口范围的元素。

具体来说,在main function中要实现:

  • push:将当前元素加入到队列中,并移除所有小于当前元素的队列末尾元素,以保持队列中元素的单调递减顺序。(如图所示,假设我们现在在选拔篮球队的队员,要求是找到最高的一位队员,5的个子比较高,所以在5出现之后他前面的队员已经失去了被选上的资格了,这就是为什么我们可以直接把前面的元素pop掉的原因。

  • pop :当窗口滑动时,我们需要移除窗口的左端元素。如果这个左端元素是队列的第一个元素(也就是当前窗口的最大值),我们需要将它从队列中弹出。但是,这并不是因为后面的数比第一个大,而是因为这个元素已经不再属于当前窗口了。

4.具体示例:

假设我们有数组 [8, 9, 6, 5, 4, 3],窗口大小 k = 3:

-初始状态:

  • 队列状态:[ ]
  • 数组:[8, 9, 6, 5, 4, 3]

-窗口 [8, 9, 6]:

  • 插入 8:[8]
  • 插入 9:移除 8,然后插入 9:[9]
  • 插入 6:[9, 6]
  • 最大值:9

-窗口向前滑动,变为 [9, 6, 5]:

  • 移除 8(它已经不在当前窗口中,不在队列中所以不需要实际操作)
  • 插入 5:[9, 6, 5]
  • 最大值:9

-窗口向前滑动,变为 [6, 5, 4]:

  • 移除 9(它是队列的第一个元素):[6, 5]
  • 插入 4:[6, 5, 4]
  • 最大值:6

-窗口向前滑动,变为 [5, 4, 3]:

  • 移除 6(它是队列的第一个元素):[5, 4]
  • 插入 3:[5, 4, 3]
  • 最大值:5


5.总结:

当我们从 deque 中弹出第一个元素时,这是因为这个元素已经不在当前滑动窗口中,而不是因为队列中有比它更大的元素。队列中始终保持从大到小的顺序,这样第一个元素始终是当前窗口的最大值。

❌易错点/难点总结:


1.n == self.maxq[0]

个人认为,本题的难点在于不好理解和区分pop和push操作的分工,很容易混淆。在pop函数中,删去最左边元素的前提条件是“n == self.maxq[0]”,很多人看到都蒙了。为啥相等的时候还要popleft?这是因为,这个过程确保了队列中的第一个元素始终是当前窗口的最大值。当窗口滑动并且最左侧的元素恰好是最大值时,我们需要将其移除,以便下一次调用 max() 方法时返回正确的新最大值。

如果 n 不是当前窗口的最大值,那么它已经被之前的 push 操作删除了,不会在队列中,所以我们不需要做任何操作。因此,目前唯一需要考虑到的一点是,当 n 是当前窗口的最左侧元素时(即 n == self.maxq[0]),这意味着 n 是当前窗口的最大值,并且已经被记录到结果列表中。所以我们可以去滑到下一个窗口去寻找新的最大值了。由于窗口向右滑动, n 不再是下一个窗口的一部分,因此需要将其从队列中移除。换句话讲,目前窗口已经满员了,所以,即使n是最左侧的最大值,我们也要把它先抛弃,这样才能给新的元素留出位置来。只有这样,新的窗口可以正确地记录新的最大值。

2.为什么 n 一定是当前窗口的最大值

  • 队列的维护:在 push 操作中,任何小于 n 的元素都会被移除,这确保了队列中的元素始终是递减的。
  • 最大值的保持:在 max 操作中,我们返回 self.maxq[0],即队列的第一个元素,它是当前窗口的最大值。
  • 移除旧值:在 pop 操作中,如果要移除的值 n 等于 self.maxq[0],这意味着当前窗口的最大值已经不再在窗口中了,因此需要将其移除,以便让新的最大值成为 self.maxq[0]。

📸解题代码:


该解决方案的时间复杂度为 O(n),其中 n 是输入数组 nums 中的元素个数。这是因为我们会对数组中的每个元素遍历一次。

空间复杂度为 O(k),其中 k 是滑动窗口的大小。这是因为 MonotonicQueue 数据结构在任何时候最多只能存储 k 个元素。

from collections import deque
class MonotonicQueue:
    def __init__(self):
        self.maxq = deque()

    def push(self, n):
        # 将小于 n 的元素全部删除
        while self.maxq and self.maxq[-1] < n:
            self.maxq.pop()
        # 然后将 n 加入尾部
        self.maxq.append(n)

    def max(self):
        return self.maxq[0]

    def pop(self, n):
        if n == self.maxq[0]:
            self.maxq.popleft()

class Solution:
    def maxSlidingWindow(self, nums, k):
        window = MonotonicQueue()
        res = []

        for i in range(len(nums)):
            if i < k - 1:
                # 先填满窗口的前 k - 1
                window.push(nums[i])
            else:
                # 窗口向前滑动,加入新数字
                window.push(nums[i])
                # 记录当前窗口的最大值
                res.append(window.max())
                # 移出旧数字
                window.pop(nums[i - k + 1])
        return res

📝347.前 K 个高频元素 (Medium)

大/小顶堆的应用, 在C++中就是优先级队列

本题是 大数据中取前k值 的经典思路,了解想法之后,不算难。

题目链接/文章讲解/视频讲解:代码随想录

未完待续。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值