队列(Queue)
特点:和栈不同,队列的最大特点是先进先出(FIFO),就好像按顺序排队一样。对于队列的数据来说,我们只允许在队尾查看和添加数据,在队头查看和删除数据。
实现:可以借助双链表来实现队列。双链表的头指针允许在队头查看和删除数据,而双链表的尾指针允许我们在队尾查看和添加数据。
应用场景:直观来看,当我们需要按照一定的顺序来处理数据,而该数据的数据量在不断地变化的时候,则需要队列来帮助解题。在算法面试题当中,广度优先搜索(Breadth-First Search)是运用队列最多的地方,我们将在第 06 课时中详细介绍。
双端队列(Deque)
特点:双端队列和普通队列最大的不同在于,它允许我们在队列的头尾两端都能在 O(1) 的时间内进行数据的查看、添加和删除。
实现:与队列相似,我们可以利用一个双链表实现双端队列。
应用场景:双端队列最常用的地方就是实现一个长度动态变化的窗口或者连续区间,而动态窗口这种数据结构在很多题目里都有运用。
例题分析
LeetCode 第 239 题:给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口 k 内的数字,滑动窗口每次只向右移动一位。返回滑动窗口最大值。
注意:你可以假设 k 总是有效的,1 ≤ k ≤ 输入数组的大小,且输入数组不为空。
示例:给定一个数组以及一个窗口的长度 k,现在移动这个窗口,要求打印出一个数组,数组里的每个元素是当前窗口当中最大的那个数。
输入:nums = [1, 3, -1, -3, 5, 3, 6, 7],k = 3
输出:[3, 3, 5, 5, 6, 7]
思路 :利用一个双端队列(保持队列的单调递增性)来保存当前窗口中最大那个数在数组里的下标,双端队列新的头就是当前窗口中最大的那个数。通过该下标,可以很快地知道新的窗口是否仍包含原来那个最大的数。如果不再包含,我们就把旧的数从双端队列的头删除。
因为双端队列能让上面的这两种操作都能在 O(1) 的时间里完成,所以整个算法的复杂度能控制在 O(n)。
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if(nums == null || k == 0){
return new int[0];
}
//最终返回的结果集长度为nums.length - k + 1
int[] res = new int[nums.length - k + 1];
Deque<Integer> queue =new LinkedList<>();
int index = 0;
for(int i = 0; i < nums.length; i++){
//删除过期元素
while(!queue.isEmpty() && (i - queue.peekFirst() >= k)){
queue.pollFirst();
}
//保持队列的单调递增性:从队尾开始比较
while(!queue.isEmpty() && nums[i] > nums[queue.peekLast()]){
queue.pollLast();
}
queue.addLast(i);
if(i >= k - 1){
res[index ++] = nums[queue.peekFirst()];
}
}
return res;
}
}