之前讲的都是栈的应用,这次该是队列的应用了。
本题算比较有难度的,需要自己去构造单调队列,建议先看视频来理解。
题目链接:239. 滑动窗口最大值
文章讲解/视频讲解:239. 滑动窗口最大值
主要思想:队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的。
双端队列的实现:LinkedList以及ArrayDeque以及常用方法不熟悉
一般ArrayDeque实现队列效率高一些,但是这道题频繁进行插入删除操作,个人觉得用LinkedList更合适一些?!
单调队列的维护
Deque中直接子类有两个:LinkedList以及ArrayDeque
ArrayDeque是无初始容量的双端队列,LinkedList则是双向链表
在Deque中,获取并移除元素的方法有两个,分别是removeXxx以及peekXxx。
存在元素时,两者的处理都是一样的。
但是当Deque内为空时,removeXxx会直接抛出NoSuchElementException,
而peekXxx则会返回null。
所以无论在实际开发或者算法时,推荐使用peekXxx方法。
其实ArrayDeque和LinkedList都可以作为栈以及队列使用,但是从执行效率来说,ArrayDeque作为队列,以及LinkedList作为栈使用,会是更好的选择。
注意:
ArrayDeque 是 Deque 接口的一种具体实现,是依赖于可变数组来实现的。ArrayDeque 没有容量限制,可根据需求自动进行扩容。ArrayDeque 可以作为栈来使用,效率要高于 Stack。ArrayDeque 也可以作为队列来使用,效率相较于基于双向链表的 LinkedList 也要更好一些。注意,ArrayDeque 不支持为 null 的元素。
在数据结构上,LinkedList 不仅实现了与 ArrayList 相同的 List 接口,还实现了 Deque 接口
当作为队列使用时,我们会将它与LinkedList 类来做对比。因为 Deque接口继承自 Queue接口,在这里,分别列出两者接口所定义的方法,两者内容区别如下:
参考文章
Java中的queue和deque、ArrayDeque
Java集合常用方法及总结
// b站帅地的代码 美团,腾讯,字节常考题
// 用队列和指针实现
// 时间复杂度:O(n),其中 n 是数组 nums 的长度。每一个下标恰好被放入队列遍历一次,并且最多被弹出队列一次,因此时间复杂度为 O(n)
// 空间复杂度:O(k)。我们使用的辅助队列这个数据结构是双向的,因此「不断从队首弹出元素」保证了队列中最多不会有超过k+1 个元素,因此队列使用的空间为 O(k)
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if(nums == null || nums.length <= 1){
return nums;
}
ArrayDeque<Integer> queue = new ArrayDeque<>();
// LinkedList queue = new LinkedList<>(); // arrayList, LinkedList, queue都能实现队列,要找一种自己熟悉的掌握
int[] res = new int[nums.length - k + 1];
int index = 0;
for(int i = 0; i < nums.length; i++){
// 判断队尾元素是否比新加进来的元素要小 queue.peekLast()队尾元素
while(!queue.isEmpty() && nums[queue.peekLast()] <= nums[i]){
queue.pollLast(); // 队尾元素出队!
}
queue.add(i); // 等价addLast,在队尾加入元素
// 去除滑动窗口外的元素
if(queue.peekLast() - k == queue.peek()){
queue.poll(); // 删除队首元素
}
// 开始记录结果
if(i + 1 >= k)
res[index++] = nums[queue.peek()]; // 队头永远是保存滑动窗口最大值
}
return res;
}
}
大/小顶堆的应用, 在C++中就是优先级队列
本题是 大数据中取前k值 的经典思路,了解想法之后,不算难。
题目链接:347.前 K 个高频元素
文章讲解/视频讲解:347.前 K 个高频元素
如何求每个元素出现的频率
如何对频率进行排序并得到出现频率前k高的元素
我们要用小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。
小顶堆实现
先用哈希表统计每个元素出现次数
再用小顶堆实现只得到出现频率前k高的元素:维护一个里面只有k个元素的小顶堆
步骤如下:
// 自己代码 也是哈希表+堆
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
for(int i = 0; i < nums.length; i++){
map.put(nums[i], map.getOrDefault(nums[i], 0) + 1);
}
// int[] 的第一个元素代表数组的值,第二个元素代表了该值出现的次数
// 优先队列建立小顶堆 方式1
// PriorityQueue pq = new PriorityQueue<>((o1, o2) ->map.get(o1) - map.get(o2));
// 方式2
// 遍历map,用最小堆保存频率最大的k个元素 这样写比方式1快很多
PriorityQueue<Integer> pq = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer a, Integer b) {
return map.get(a) - map.get(b);
}
});
for (Integer key : map.keySet()) { //小顶堆只需要维持k个元素有序
if (pq.size() < k) { //小顶堆元素个数小于k个时直接加
pq.add(key);
} else if(map.get(key) > map.get(pq.peek())){
//当前元素出现次数大于小顶堆的根结点(这k个元素中出现次数最少的那个)
pq.poll(); //弹出队头(小顶堆的根结点),即把堆里出现次数最少的那个删除,留下的就是出现次数多的了
pq.add(key);
}
}
int[] ans = new int[k];
for (int i = k - 1; i >= 0; i--) { //依次弹出小顶堆,先弹出的是堆的根,出现次数少,后面弹出的出现次数多
ans[i] = pq.poll();
}
return ans;
}
}
栈与队列总结篇
栈与队列的基本操作:栈实现队列
,用队列实现栈
栈在系统中的应用:括号匹配问题
、字符串去重问题
、逆波兰表达式问题
两种队列的应用:单调队列和优先级队列:滑动窗口最大值
,前K个高频元素
(介绍了两种队列:单调队列和优先级队列,这是特殊场景解决问题的利器,是一定要掌握的。)