目录
本篇内容是在官方文档的基础上的个人解释旨在对双端队列进行解释
引言
在数据结构和算法的世界里,面对不同的问题,选择合适的数据结构是至关重要的。本文通过解决一个具体的问题——寻找滑动窗口的最大值,来展示如何高效地使用双端队列(Deque)。我们将详细解释双端队列的工作原理,并展示其在解决这个问题时的应用。
题目链接:滑动窗口的最大值_牛客题霸_牛客网 (nowcoder.com)
本篇内容是在官方文档的基础上的个人解释旨在对双端队列进行解释
问题描述
给定一个长度为 n 的数组 num
和滑动窗口的大小 size
,需要找出所有滑动窗口里数值的最大值。例如,对于数组 {2,3,4,2,6,2,5,1}
和滑动窗口大小为 3,我们的目标是找出每个窗口的最大值,即 {4,4,6,6,6,5}
。
双端队列(Deque)原理
在深入解决方案之前,首先理解双端队列是必要的。双端队列是一种具有队列和栈性质的抽象数据类型。它允许在队列的两端进行插入和删除操作,非常适合用于需要两端操作的场景。
From.Java ArrayDeque - Java教程 - 菜鸟教程 (cainiaojc.com)
从图中我们可以看出,双端队列允许在队列的前端和后端进行以下操作:
-
添加元素(
addFirst
、addLast
) -
删除元素(
removeFirst
、removeLast
) -
访问元素(
getFirst
、getLast
)
在 ArrayDeque(双端队列)中,"队首"和"队尾"是相对的概念,取决于你如何使用队列。如果你选择从右侧(末尾)入队,那么在这种情况下,右侧就可以被视作队尾,左侧则是队首。
具体来说:
- 当你从右侧(末尾)使用 addLast 或 offerLast 方法向队列添加元素时,你实际上是在队尾添加元素。
- 相应地,左侧变成了队首。你可以使用 removeFirst 或 pollFirst 来从队首移除元素,或使用 peekFirst 来查看队首元素而不移除它。
重要的是要记住,无论你从哪一端开始添加元素,ArrayDeque 都允许你在另一端进行操作,这就是双端队列的灵活之处。所以,队首和队尾的概念将根据你的使用方式(即从哪一端添加或移除元素)而确定。
解决方案
初始化
首先,检查窗口大小是否合理(即不超过数组长度且不为0)。如果不合理,返回空结果。
使用双端队列处理窗口
我们使用双端队列来存储窗口中所有元素的索引,并保持队列中的元素值是递减的。这样,队列的头部始终是当前窗口的最大值。
窗口滑动过程
-
初始化窗口:首先处理滑动窗口的第一个位置。遍历窗口内的元素,确保双端队列中元素值递减。
-
滑动窗口:随着窗口向右滑动,更新双端队列。
-
如果队列头部的元素不在窗口内(即索引小于当前索引减去窗口大小),则将其从队列中移除。
-
从队列尾部移除所有小于当前元素值的索引,然后将当前元素的索引添加到队列尾部。
-
-
记录最大值:每次滑动后,队列的头部元素即为当前窗口的最大值。
实现代码
From.官方题解
import java.util.*;
public class Solution {
public ArrayList<Integer> maxInWindows(int [] num, int size) {
ArrayList<Integer> res = new ArrayList<Integer>();
//窗口大于数组长度的时候,返回空
if(size <= num.length && size != 0){
//双向队列
ArrayDeque <Integer> dq = new ArrayDeque<Integer>();
//先遍历一个窗口
for(int i = 0; i < size; i++){
//去掉比自己先进队列的小于自己的值
while(!dq.isEmpty() && num[dq.peekLast()] < num[i])
dq.pollLast();
dq.add(i);
}
//遍历后续数组元素
for(int i = size; i < num.length; i++){
//取窗口内的最大值
res.add(num[dq.peekFirst()]);
while(!dq.isEmpty() && dq.peekFirst() < (i - size + 1))
//弹出窗口移走后的值
dq.pollFirst();
//加入新的值前,去掉比自己先进队列的小于自己的值
while(!dq.isEmpty() && num[dq.peekLast()] < num[i])
dq.pollLast();
dq.add(i);
}
res.add(num[dq.pollFirst()]);
}
return res;
}
}
ArrayDequeue方法说明
队列首部(Front)的操作
-
addFirst(E e)
: 在双端队列的前面添加一个元素。如果双端队列已满,此方法将抛出一个IllegalStateException
。 -
offerFirst(E e)
: 在双端队列的前面添加一个元素。如果双端队列已满,则返回false
。 -
removeFirst()
: 移除并返回双端队列的第一个元素。如果双端队列为空,此方法将抛出一个NoSuchElementException
。 -
pollFirst()
: 移除并返回双端队列的第一个元素。如果双端队列为空,则返回null
。 -
peekFirst()
: 返回双端队列的第一个元素,但不移除它。如果双端队列为空,则返回null
。
队列尾部(Last)的操作
-
addLast(E e)
: 在双端队列的末尾添加一个元素。如果双端队列已满,此方法将抛出一个IllegalStateException
。 -
offerLast(E e)
: 在双端队列的末尾添加一个元素。如果双端队列已满,则返回false
。 -
removeLast()
: 移除并返回双端队列的最后一个元素。如果双端队列为空,此方法将抛出一个NoSuchElementException
。 -
pollLast()
: 移除并返回双端队列的最后一个元素。如果双端队列为空,则返回null
。 -
peekLast()
: 返回双端队列的最后一个元素,但不移除它。如果双端队列为空,则返回null
。
总结
通过使用双端队列,我们可以高效地解决滑动窗口的最大值问题。双端队列的灵活性在于能够从两端对队列进行操作,这使得它在许多场景下都非常有用,尤其是在需要快速访问头部和尾部元素的情况下。