239.滑动窗口最大值
题目链接:https://leetcode.cn/problems/sliding-window-maximum/description/
视频链接:https://www.bilibili.com/video/BV1XS4y1p7qj/?spm_id_from=333.788&vd_source=80cf8293f27c076730af6c32ceeb2689
讲解链接:https://programmercarl.com/0239.%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E6%9C%80%E5%A4%A7%E5%80%BC.html
思路
- 暴力法:遍历一遍的,每次从窗口中再找到最大的数值。这样是O(n*k)
能不能用大顶堆(优先队列)来存放窗口里的k个数字?
- 不行。窗口是移动的,而大顶堆每次只能弹出最大值,不能移除其他数字。这样大顶堆维护的就不是窗口里的数字了。
我们需要一个队列,这个队列放进去窗口里的元素,然后随着窗口的移动,队列也一进一出,每次移动之后,队列告诉我们里面的最大值是什么。—单调队列 这个队列没有必要维护
有三个方法:
push() --处理右边要新加进来的元素
pop() --处理左边要遗弃掉的元素
getMax() ---实际就是返回队头元素
每移动一次窗口就调用一次这三个方法,getMax得到的都存储在一个数组里
举例:1 3 -1 -3 5 3 2 1
k=3
第一次:单调队列里没有必要维护3之前 比3还小的元素。(3既比1大,又比1活得久,所以1就没用了)3后面的-1可以放进去。
- push()的规则?如果push进来的元素 比 处于队头的都要大,前面的都要排除。直到队头元素没有比要push进来的元素大为止。如果比前面的小,放进去就行。新进来的如果比当前单调队列队尾的大,就把前面的全部踢走
- pop()的规则?如果要pop的元素等于当前窗口里的最大值(队头),就要弹出。
- getMax() 实际就是返回队头就行。
关键:单调队列里维护的最大值始终是队头(出口处)元素!有潜力成为最大值的才会加入到队列里。)
-
队列出口元素:deque.peek()
-
队列入口元素:deque.getLast()
-
注意在每次要进行操作前都要先判空,避免空指针异常!
-
先看是否nums只有一个元素,是就直接返回。然后初始化单调队列,把第一次放进去。
class MyQueue {
Deque<Integer> deque = new LinkedList<>();
//弹出元素时,比较当前要弹出的数值是否等于队列出口的数值,如果相等则弹出
void poll(int val) {
if (!deque.isEmpty() && val == deque.peek()) {
deque.poll();
}
}
//添加元素时,如果要添加的元素大于入口处的元素,就将入口元素弹出
void add(int val) {
while (!deque.isEmpty() && val > deque.getLast()) {
deque.removeLast();
}
deque.add(val);
}
int peek() {
return deque.peek();
}
}
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if (nums.length == 1) {
return nums;
}
int len = nums.length - k + 1;
//存放结果元素
int[] res = new int[len];
int num = 0;
//初始化单调队列,先将前k的元素放入队列
MyQueue myQueue = new MyQueue();
for (int i = 0; i < k; i++) {
myQueue.add(nums[i]);
}
//第一个窗口里的最大值
res[num++] = myQueue.peek();
for (int i = k; i < nums.length; i++) {
//滑动窗口移除左边的元素
myQueue.poll(nums[i - k]);
//滑动窗口加入右侧的元素
myQueue.add(nums[i]);
//记录对应的最大值
res[num++] = myQueue.peek();
}
return res;
}
}
347.前 K 个高频元素
题目链接:https://leetcode.cn/problems/top-k-frequent-elements/
视频链接:https://www.bilibili.com/video/BV1Xg41167Lz/?spm_id_from=333.788&vd_source=80cf8293f27c076730af6c32ceeb2689
讲解链接:https://programmercarl.com/0347.%E5%89%8DK%E4%B8%AA%E9%AB%98%E9%A2%91%E5%85%83%E7%B4%A0.html
思路
考虑三个内容:
- 要统计元素出现频率
- 对频率排序
- 找出前K个高频元素
先遍历一次所有元素,用map集合来统计出现的次数。key=元素,value=出现次数
- 怎么统计?常用套路:
Map<Integer, Integer> map = new Map<>();
for(int num : nums) {
//根据key查询对应value,查不到就返回传的默认值(第二个参数)
map.put(num,map.getOrDefault(num,0)+1);
}
我们其实没有必要对所有元素(出现的次数)进行排序!只要维护前k个—大顶堆和小顶堆。
- 大顶堆:根节点是最大的(底层是二叉树)
- 小顶堆:根节点是最小的
我们只要用它遍历一遍所有元素,维护它的规模是k就行。用哪种堆呢?
- 第一反应可能是用大顶堆。如果是大顶堆,假如要加进来一个元素,就要弹出一个。而堆这种结构要弹出都是从堆顶弹出的。用大顶堆来遍历,每次弹出的都是堆里当前最大的那个,所以最后剩下的是前k个小的了。
- 所以我们用小顶堆。每次不断把堆里最小的元素移除
时间复杂度
如果对所有元素进行排序,时间复杂度是O(n*logn)
在这个k大小的堆里调整元素,时间复杂度是O(logk)。因此总的时间复杂度是O(n*logk)
关于map和优先队列
我们这里的小根堆(优先队列)每个节点是一个长度为2的数组![num,cnt]
pq.peek()
就是在堆顶的元素,一个二元组 pq.peek()[1]
就是出现的次数!
- java中小/大顶堆采用优先队列实现:
默认是小根堆,大根堆需要用户提供比较器:new PriorityQueue<>(Collections.reverseOrder())
比较器可以写成lambda表达式形式。 o2-o1是大根 o1-o2是小根!
- 如何遍历?
//创建小根堆,传入lambda表达式,每个元素是一个长度为2的数组[num,cnt]
PriorityQueue<int[]> pq = new PriorityQueue<>((pair1,pair2)->pair1[1]-pair2[1]);
for(Map.Entry<Integer,Integer> entry : map.entrySet()){ //用entry来遍历
if(pq.size()<k){ //当小顶堆中元素个数小于k个时 直接添加进堆
pq.add(new int[]{entry.getKey(),entry.getValue()});
}else{
if(entry.getValue()>pq.peek()[1]){//当前元素出现次数大于小顶堆的根结点(这k个元素中出现次数最少的那个)
pq.poll();//弹出队头(小顶堆的根结点),即把堆里出现次数最少的那个删除,留下的就是出现次数多的了
pq.add(new int[]{entry.getKey(),entry.getValue()});
}
}
}
完整java代码
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
for(int num : nums) {
//根据key查询对应value,查不到就返回传的默认值(第二个参数)
map.put(num,map.getOrDefault(num,0)+1);
}
//在优先队列中存储二元组(num,cnt),cnt表示元素值num在数组中的出现次数
//下面这个是lambda表达式 比较的是pair[1]也就是出现次数 o2-o1是大根 o1-o2是小根!
PriorityQueue<int[]> pq = new PriorityQueue<>((pair1,pair2)->pair1[1]-pair2[1]);
for(Map.Entry<Integer,Integer> entry : map.entrySet()){ //小顶堆只需要维持k个元素有序
if(pq.size()<k){//小顶堆元素个数小于k个时直接加
pq.offer(new int[]{entry.getKey(),entry.getValue()});
}else{
if(entry.getValue()>pq.peek()[1]){//当前元素出现次数大于小顶堆的根结点(这k个元素中出现次数最少的那个)
pq.poll();//弹出队头(小顶堆的根结点),即把堆里出现次数最少的那个删除,留下的就是出现次数多的了
pq.offer(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;
}
}
栈与队列小总结
-
比较一下队列和栈相关方法的不同:
队列:
add():在队尾添加元素
remove():删除并返回队头
isEmpty():是否为空栈:
push():压入元素
pop():弹出栈顶元素,并返回栈顶元素
peek():返回栈顶元素
empty():是否为空
其他求长度size()
,求堆头/栈顶peek()
和清空clear()
都是一样的!
-
注意在java中,栈是一个实现类。而队列是一个接口,实现类有LinkedList和PriorityQueue
-
关于deque
队头元素:deque.peek()
队尾元素:deque.getLast()
加入元素:deque.add()
移除元素:deque.poll()
- 关于map的遍历
用.entryset()方法 把Map转换成Set集合
HashMap集合转化成Set集合后,Set集合中每一个元素都是Map.Entry<Integer,String> 这个类型的数据
Map.Entry<K,V> 表示一个映射项
可以通过entry.getValue()
和 entry.getKey()
获得对应的value或者key
- 优先队列的使用:
默认是小根堆,大根堆需要用户提供比较器:new PriorityQueue<>(Collections.reverseOrder())
比较器可以写成lambda表达式形式。 o2-o1是大根 o1-o2是小根!
q.size(); //获取元素个数
q.offer(); //插入元素 add()也可以
q.peek(); //获取优先级最高的元素
q.poll(); //移除优先级最高的元素 并返回
q.isEmpty(); //判空