今天还学习了如何使用Java官方文档,也称得上具有里程碑的意义吧,至少比去csdn翻别人复制过来的代码强很多啦😆
注册oracle时起了一个还起了一个新的名字:
Lucien-Hector
虽然Hector在当下的英文中意为暴徒,但在希腊神话中是一个勇敢、重情、缜密的形象,还是挺不错的个英文名吧=-=
题目链接:239. 滑动窗口最大值 - 力扣(LeetCode)
第一道Hard题目啊hhhhhhh
初见还是没有什么思路,只知道大概要使用队列进行操作,并有着弹出 加入元素的判断,但若是正好弹出了最大的元素,滑动窗口内的最大值还是需要遍历求解,这点困扰住了我,(←看过题解后知道思路为保持队列最前端为最大值,这样就不用遍历了)所以还是去看了代码随想录中的解析
以下为解题思路:
单调队列堂堂登场!
本题的难点在于动态窗口中是会移除一个元素的,而这个元素可能为最大值,也可能为其他的任意一个值,因此处理这道题目需要使用到我们的单调队列,这个单调队列能够告诉我们在滑动窗口中的最大值是多少~
重点来咯:队列如何在能够告诉我们最大值是多少的同时还能够弹出一个窗口要移除的元素呢?
核心思想→其实队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队里里的元素数值是由大到小的。
那么这个维护元素单调递减的队列就叫做单调队列,即单调递减或单调递增的队列。
不要以为实现的单调队列就是 对窗口里面的数进行排序,如果排序的话,那和优先级队列又有什么区别了呢。
单调队列具体的情况如下面的动画:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0uDKsgom-1668495373413)(https://code-thinking.cdn.bcebos.com/gifs/239.%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E6%9C%80%E5%A4%A7%E5%80%BC.gif)]
设计单调队列的时候,pop,和push操作要保持如下规则:
- poll(value):除非窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作
- add(value):add时除去比需要添加的元素更小的元素
保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。
为了更直观的感受到单调队列的工作过程,以题目示例为例,输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3,动画如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eKLXbDP8-1668495373414)(https://code-thinking.cdn.bcebos.com/gifs/239.%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E6%9C%80%E5%A4%A7%E5%80%BC-2.gif)]
则我们的单调队列应当这样定义:
class MonQueue{
Deque<Interger> queue = new LinkedList<>();
//删除元素时,若要删除的元素值和队列最前端值相同时则删去队列前端值
void poll(int val){
if(!queue.isEmpty() && val == queue.peek()){
queue.poll();
}
//添加元素时,删去比添加的元素值还小的队列中的值,然后添加该元素
void add(int val){
while(!queue.isEmpty() && val > queue.getLast()){
queue.removeLast();
}
queue.add(val);
}
//直接返回队列最前端的元素即可
void peek(){
return queue.peek();
}
}
//这样就完成了我们对整个单调队列的定义
则ac代码如下:
class Solution {
class MonQueue {
Deque<Integer> queue = new LinkedList<>();
void poll(int val) {
if (!queue.isEmpty() && val == queue.peek()) {
queue.poll();
}
}
void add(int val) {
while (!queue.isEmpty() && val > queue.getLast()) {
queue.pollLast();
}
queue.add(val);
}
int peek() {
return queue.peek();
}
}
public int[] maxSlidingWindow(int[] nums, int k) {
if(nums.length == 1) return nums;
MonQueue queue = new MonQueue();
for(int i = 0; i < k;i++){
queue.add(nums[i]);
}
int num = 0;
int res = new int[nums.length()-k+1];
res[num++] = queue.peek();
for(int i = k; i < nums.length;i++){
queue.poll(nums[i-k]);
queue.add(nums[i]);
res[num++] = queue.peek();
}
return res;
}
}
处理方法很巧妙,单调队列只维护需要被维护的元素,本题中若删除的元素小于队列中的最大值则不做处理,添加元素时则除去比其更小的元素后再添加,(有点像hector?)
题目链接:347. 前 K 个高频元素 - 力扣(LeetCode)
初见思路:使用map来处理这道问题,随即根据value中的值进行排序,返回前k个值就好,时间复杂度为O(n logn)
上述思路的代码我写不出来55,等二刷的时候再来实现8
本题的最优思路为使用优先队列来实现,即使用堆来完成
本题的主要内容为:
- 统计元素出现频率(使用map解决)
- 对频率进行排序
- 找出前k个高频元素
在第二步中,我们使用优先队列来进行排序
什么是优先级队列呢?
其实就是一个披着队列外衣的堆,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。
而且优先级队列内部元素是自动依照元素的权值排列。那么它是如何有序排列的呢?
缺省情况下priority_queue利用max-heap(大顶堆)完成对元素的排序,这个大顶堆是以vector为表现形式的complete binary tree(完全二叉树)。
什么是堆呢?
堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。
所以大家经常说的大顶堆(堆头是最大元素),小顶堆(堆头是最小元素),如果懒得自己实现的话,就直接用priority_queue(优先级队列)就可以了,底层实现都是一样的,从小到大排就是小顶堆,从大到小排就是大顶堆。
使用哪种堆?
本题中若使用大顶堆,则每次移动更新大顶堆的时候,每次弹出都把最大的元素弹出去了,无法保留下来前K个高频元素。
因此使用小顶堆,其流程如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Pmlhtgbl-1668495401708)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e4022f4d-500b-46c0-a193-d4f75d92f8fd/Untitled.png)]
代码实现如下:
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer,Integer> map = new HashMap<>();
for(int num:nums){
map.put(num,map.getOrDefault(num,0)+1);
}
PriorityQueue<int[]> pq = new PriorityQueue<>((pair1,pair2)->pair1[1] - pair2[1]);
for(Map.Entry<Integer,Integer> entry:map.entrySet()){
if (pq.size() < k){
pq.add(new int[]{entry.getKey(),entry.getValue()});
}else{
if(entry.getValue()>pq.peek()[1]){
pq.poll();
pq.add(new int[]{entry.getKey(),entry.getValue()});
}
}
}
int[]ans = new int[k];
for(int i = k-1;i>=0;i--){
ans[i] = pq.poll()[0];
}
return ans;
}
}
说实话代码只看了个大概懂,还是得继续加油鸭