前言
在最近的文章里,我深入总结了滑动窗口算法的精髓。如果你觉得这还不够刺激,那么来看看单调队列的讲解吧,它将进一步拓展你对滑动窗口认识的深度。另外,文章末尾还隐藏着一个掘金的小秘密,千万不要错过哦😂😂!
算法简介(来源于科大讯飞)
单调队列是一种具有单调性质的数据结构,主要用于在动态变化的区间内快速获取最值问题。
单调队列分为单调递增和单调递减两种类型,这种数据结构并不是STL(标准模板库)中的内容,而是通过特定的数据结构实现的。下面将具体介绍单调队列的各个方面:
- 基本概念
- 定义:单调队列是一种队列,其中的元素始终保持某种单调性(即递增或递减)。这意味着队首元素总是当前区间内的最小值或最大值。
- 实现方式:通常使用数组来模拟单调队列,通过对数组元素的管理保持其单调性。在C++代码实现中,可以使用双端队列
deque来实现单调队列。
- 操作方法
- 入队操作:新元素进入队列时,需要保持队列的单调性。如果新元素大于队尾元素,则可以直接入队;如果小于队尾元素,则需要依次出队直到可以维护单调性为止。
- 出队操作:当队首元素超出当前考虑的区间时,需要将其出队,以保持队列元素的有效性。
- 应用场景
- 滑动窗口问题:这类问题涉及在连续区间内查找最值。例如,给定一个数组和一个窗口大小,求每个滑动窗口内的最大值或最小值。单调队列通过其单调性质,可以有效地解决这个问题[。
- 区间最值问题:需要在动态变化的区间内快速查询最值。例如,对于数组
[7, 6, 8, 12, 9, 10, 3],在每个(i-4, i)的区间内寻找最小值,单调队列可以在O(n)时间内完成此类查询。
- 优势对比
- 与优先队列的比较:虽然优先队列(堆)也可以用于解决滑动窗口问题,但单调队列在特定情况下更高效。优先队列的时间复杂度为
O(log n),而单调队列可以做到O(1)的入队和出队,从而在整体上达到O(n)的时间复杂度。 - 与暴力解法的比较:暴力解法通常涉及多次遍历和重复计算,时间复杂度较高。单调队列通过维持区间的单调性,避免了不必要的计算,大幅提高了效率。
- 与优先队列的比较:虽然优先队列(堆)也可以用于解决滑动窗口问题,但单调队列在特定情况下更高效。优先队列的时间复杂度为
- 注意事项
- 队首队尾操作:在实现过程中,需要特别注意队首和队尾的指针移动。队尾只允许插入操作,队首和队尾都可以进行删除操作。
- 空间管理:尽管单调队列可以用数组模拟,但是要注意防止空间滥用,避免不必要的内存开销。
总之,单调队列作为一种具有单调性质的数据结构,广泛应用于求解滑动窗口和区间最值问题。它通过保持队列元素的单调性,在O(n)的时间复杂度内完成任务,比传统的优先队列和其他暴力解法更加高效。理解和应用单调队列,可以显著提高解决一类区间最值问题的效率。
大白话讲算法
要深入理解单调队列,首先需要掌握两个核心概念:单调性和队列。单调性指的是按照某种特定规律持续变化的过程。在我们的日常生活中,单调性通常体现为两种形式:单调递增和单调递减。例如,人的年龄逐年增长就是一个单调递增的过程,而炎炎夏日里雪糕不断变小则是一个单调递减的例子。
接下来,让我们探讨一下队列的概念。可以将队列想象成一个具有入口和出口的管道,在这个管道中,元素只能从一端(头部)进入并从另一端(尾部)离开。
将单调性和队列这两个概念结合起来,我们得到了单调队列。想象一下,在一个管道中排列着许多人,这些人从管道的尾部到头部年龄逐渐增大。这种按年龄顺序排列的人群就可以被抽象为一个单调队列。在单调队列中,无论队列是递增还是递减,元素的排列都遵循着这种单调性,即每个元素都不会打破这个序列的有序状态。
漫画展示流程
单调队列可以理解为特殊的窗口,在这个窗口所有的元素是具有单调性的
设想这样一个场景:一行人群中,每人手持不同数量的苹果。突然,一位有影响力的人物要求你确定每连续三人中谁持有的苹果最多。面对这个任务,你会如何高效地找到答案呢?
就在这时,意外的一幕出现了:一只蓝色的狸猫不知从哪儿冒出来,它从自己肚子前的口袋里掏出了一个叫做“单调队列”的神奇道具。你惊奇地发现这个口袋异常巨大,不禁好奇地问道:“它的口袋怎么那么大呀?”
单调队列转向你,并发出亲切的声音:“亲爱的朋友,请先进行初始化设置。请告诉我你的具体需求是什么?”于是你详细地解释了大佬的要求。随后,单调队列便自动调整为单调递增模式,并将容量设置为3,准备开始它的任务。
持有一个苹果的1号走到单调递增队列前,队列随即引导他进入其内部。目前,统计人数为1,还未达到三人的要求。
接下来,当2号准备进入时,由于1号与2号的组合不满足单调递增的特性,单调递增队列毫不犹豫地淘汰了1号。因此,队列中只剩下了2号,人数统计更新为2。
当3号进入场景时,我们发现3号拥有两个苹果,这使得整个队列保持了单调递增的状态。因此,单调递增队列允许3号加入。此时,人数达到了3,符合大佬的要求。单调递增队列随后指出,在1号、2号和3号中,拥有苹果最多的是位于队列最前端的2号。
当4号想要加入时,我们发现2号和3号已不符合单调递增的条件,因此他们被单调递增队列移除。尽管如此,当前的统计人数仍然符合大佬的要求。此时,单调递增队列提出挑战,让我们猜测在[2号,3号,4号]中,谁拥有的苹果最多?答案就在我的体内哦!
随后,5号加入了单调递增队列。在[3号,4号,5号]的组合中,拥有苹果数量最多的是4号。
接下来,6号进入了单调递增队列,并导致了5号的离开。在[4号,5号,6号]这组中,拥有苹果数量最多的仍然是4号。
然后,7号进入了单调递增队列,这导致4号和6号被移除。在[5号,6号,7号]这个序列中,拥有苹果数量最多的是7号。
随后,8号解释道:“我们拥有相同数量的苹果,但如果我进来,单调递增队列的性质将不复存在。”因此,单调递增队列不得不移除7号。在[6号,7号,8号]的组合中,拥有苹果最多的是8号。然而,就在这关键时刻,大佬带来了4个新人以增加难度。
当9号加入到队列中时,我们发现在[7号,8号,9号]的组合中,拥有苹果数量最多的依然是8号。
10号进入队列后,[8号,9号,10号]组合中拥有最多苹果的仍然是8号。
11号想要进入队列时,单调递增队列回应称其容量有限,仅能容纳三个人,因此11号需要等待8号离开队列后才能进入。此时,在[9号,10号,11号]的组合中,拥有最多苹果的是9号。
当12号希望加入队列时,遇到了和11号相同的情况,这就要求9号默默地走出队列。此时,[10号,11号,12号]组合中拥有最多苹果的是10号。
最终,蓝色狸猫成功地获得了大佬的认可!
具体实例
1、滑动窗口最大值
给你一个整数数组 nums,有一个大小为 k **的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
示例 2:
输入:nums = [1], k = 1
输出:[1]
提示:
1 <= nums.length <= 105104 <= nums[i] <= 1041 <= k <= nums.length
实现代码如下(看不懂的可以回去看看漫画)
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
ans = []
q = deque() # 双端队列
for i, x in enumerate(nums):
# 1. 入
while q and nums[q[-1]] <= x:
q.pop() # 维护 q 的单调性
q.append(i) # 入队
# 2. 出
if i - q[0] >= k: # 队首已经离开窗口了
q.popleft()
# 3. 记录答案
if i >= k - 1:
# 由于队首到队尾单调递减,所以窗口最大值就是队首
ans.append(nums[q[0]])
return ans
2、绝对差不超过限制的最长连续子数组
给你一个整数数组 nums ,和一个表示限制的整数 limit,请你返回最长连续子数组的长度,该子数组中的任意两个元素之间的绝对差必须小于或者等于 limit 。
如果不存在满足条件的子数组,则返回 0 。
示例 1:
输入:nums = [8,2,4,7], limit = 4
输出:2
解释:所有子数组如下:
[8] 最大绝对差 |8-8| = 0 <= 4.
[8,2] 最大绝对差 |8-2| = 6 > 4.
[8,2,4] 最大绝对差 |8-2| = 6 > 4.
[8,2,4,7] 最大绝对差 |8-2| = 6 > 4.
[2] 最大绝对差 |2-2| = 0 <= 4.
[2,4] 最大绝对差 |2-4| = 2 <= 4.
[2,4,7] 最大绝对差 |2-7| = 5 > 4.
[4] 最大绝对差 |4-4| = 0 <= 4.
[4,7] 最大绝对差 |4-7| = 3 <= 4.
[7] 最大绝对差 |7-7| = 0 <= 4.
因此,满足题意的最长子数组的长度为 2 。
示例 2:
输入:nums = [10,1,2,4,7,2], limit = 5
输出:4
解释:满足题意的最长子数组是 [2,4,7,2],其最大绝对差 |2-7| = 5 <= 5 。
示例 3:
输入:nums = [4,2,2,2,4,4,2,2], limit = 0
输出:3
提示:
1 <= nums.length <= 10^51 <= nums[i] <= 10^90 <= limit <= 10^9
解题思路
题目要求我们找到一个连续子数组,使得该子数组内任意两个元素的绝对差都小于或等于给定的limit。为了解决这个问题,我们可以运用一个单调递增队列和一个单调递减队列。这两个队列能够帮助我们快速获取当前连续子数组中的最小值和最大值。
具体步骤如下:
- 遍历数组,对于每个元素,我们先将其添加到单调递增队列中,这将帮助我们保持队列中元素的严格递增顺序,从而队头是最小值。
- 同样地,我们也要将每个元素添加到单调递减队列中,这保证了队列中的元素是严格递减的,因此队头是最大值。
- 检查当前元素与两个队列的队头元素(即当前的最小值和最大值)的绝对差是否都不超过
limit。如果是这样,说明当前考虑的连续子数组满足条件。 - 如果当前元素导致连续子数组不满足条件(即与最小值或最大值的绝对差超过
limit),则需要调整连续子数组的起始位置,即从包含下一个元素的子数组重新开始判断。 - 在每一步,我们都记录下满足条件的最长连续子数组的长度。
通过上述方法,我们可以有效地找到满足题目要求的最长连续子数组的长度。
让我们通过漫画的形式来展示解决示例一(nums = [8, 2, 4, 7],limit = 4)的过程:
实现代码如下
class Solution:
def longestSubarray(self, nums: List[int], limit: int) -> int:
n = len(nums)
mxQ = deque() # 创建单调递增队列
miQ = deque() # 创建单调递减队列
left = res = 0 # 初始化连续数组的开始位置和数组长度
for i, v in enumerate(nums):
while mxQ and v > mxQ[-1]:
mxQ.pop()
while miQ and v < miQ[-1]:
miQ.pop()
mxQ.append(v)
miQ.append(v)
while miQ and mxQ and mxQ[0] - miQ[0] > limit: # 移动连续数组的开始位置
if nums[left] == mxQ[0]:
mxQ.popleft()
if nums[left] == miQ[0]:
miQ.popleft()
left += 1
res = max(res, i - left + 1)
return res
文章最后的碎碎念(吐槽加发现掘金的Bug)
吐槽
掘金能不能出一个将Notion的文章快速导入的功能嘞!
掘金Bug
掘金的bug,出现的场景是这样的:在利用Notion将文档转换成md格式并尝试通过掘金平台的文章导入功能发布时,由于文档中包含本地图片,不得不手动逐一上传这些图片。然而,在准备发布文章的最终阶段,却意外发现文章无法自动保存,导致发布进程受阻。
在细致地排查错误原因的过程中,我意外发现导致文章无法自动保存的竟是图片文件名中包含的英文左括号。这次文章发布过程中意外发掘出掘金平台的一个Bug,为整个过程增添了几分乐趣和惊喜。