239. 滑动窗口最大值
这题比较难,我们需要找到窗口内的最大值,并写到数组中返回。这题我们用单调队列来写。代码如下:
class Solution {
Deque<Integer> que=new LinkedList<>();
public int[] maxSlidingWindow(int[] nums, int k) {
if(nums==null||nums.length==0){
return new int[0];
}
int n=nums.length;
int[] res=new int[n-k+1];
for(int i=0;i<k;i++){
push(nums[i]);
}
res[0] = getMaxValue();
for(int i=k;i<n;i++){
pop(nums[i-k]);
push(nums[i]);
res[i-k+1]=getMaxValue();
}
return res;
}
public void pop(int val){
if(!que.isEmpty()&&val==que.peekFirst()){
que.pollFirst();
}
}
public void push(int val){
while(!que.isEmpty()&&val>que.peekLast()){
que.pollLast();
}
que.offerLast(val);
}
public int getMaxValue(){
if (que.isEmpty()) {
return -1;
}
return que.peekFirst();
}
}
我们使用一个单调递减队列来存放当前窗口中的最大元素,那么我们获得最大值时从该队列的头部获得即可。但为了保证最大的始终在头部,我们的pop和push函数要有一定的限制。
push函数:当新元素进入时,会不断从队尾删除所有比它小的元素,因为这些元素不可能再成为未来窗口的最大值。清理完成后,把新元素加入队尾。这样就保证了队列从前到后是递减的结构。
pop函数:只有当滑出的值等于队头元素时,才需要把队头弹出;如果不等,说明该元素早已在之前被更大的数“挤掉”,队列里已经没有它了,因此不需要操作。
getMaxValue 方法很简单,直接返回队头元素,因为在单调递减队列中,队头始终是当前窗口的最大值。如果队列为空则返回 -1 作为保护。
整个代码中,首先进行边界判断,如果数组为空直接返回空数组。然后创建结果数组,长度为 n-k+1。接着先把前 k 个元素依次通过 push 加入单调队列,构成第一个窗口,此时队头就是第一个窗口的最大值,记录到 res[0]。之后开始滑动窗口:每向右移动一步,先调用 pop(nums[i-k]) 移除滑出窗口的元素(如果它正好是当前最大值才真正出队),再调用 push(nums[i]) 把新进入窗口的元素加入队列,并更新当前窗口最大值到结果数组。
大顶堆和小顶堆
大顶堆和小顶堆都是一种特殊的完全二叉树结构,统称为堆。常用于快速获取最大值或最小值。大顶堆要求每个父节点的值都大于等于它的子节点,因此整棵树的最大值一定在根节点;小顶堆则相反,每个父节点都小于等于子节点,所以最小值在根节点。注意,堆并不是整体有序的,它只保证父子之间的大小关系。
在 Java 中,最常用的是 PriorityQueue。它默认是小顶堆,如果想要大顶堆,需要自定义比较器。例如:
小顶堆(默认):
PriorityQueue minHeap = new PriorityQueue<>();
minHeap.offer(5);
minHeap.offer(2);
minHeap.offer(8);
System.out.println(minHeap.peek()); // 最小值 2
大顶堆(需要比较器):
PriorityQueue maxHeap = new PriorityQueue<>((a, b) -> b - a);
maxHeap.offer(5);
maxHeap.offer(2);
maxHeap.offer(8);
System.out.println(maxHeap.peek()); // 最大值 8
347. 前 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);
}
PriorityQueue<Map.Entry<Integer,Integer>> pq=new PriorityQueue<>((a,b)->a.getValue()-b.getValue());
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
pq.offer(entry);
if (pq.size() > k) {
pq.poll();
}
}
int[] res=new int[k];
for(int i=k-1;i>=0;i--){
res[i]=pq.poll().getKey();
}
return res;
}
}
首先创建一个 HashMap,用来统计数组中每个数字的出现频率。for 循环遍历 nums 数组,对于每个元素 nums[i],通过 map.getOrDefault(nums[i], 0) 先安全获取当前已有次数(如果不存在就返回 0),然后加 1 再放回 map。执行完这一段后,map 中保存的就是“数字 → 出现次数”的映射关系。
接下来创建了一个 PriorityQueue,并指定比较器为按 value(频率)升序排列。因为 Java 的 PriorityQueue 默认是小顶堆,所以这里构建的是一个“按频率排序的小顶堆”,堆顶元素永远是当前频率最小的那个。
然后通过 for-each 循环遍历 map.entrySet(),依次拿到每一个键值对 entry(即某个数字及其出现次数),把它放入小顶堆中。每放入一个元素后,如果堆的大小超过 k,就立刻调用 poll() 弹出堆顶(也就是当前频率最小的元素)。这样做的效果是:堆中始终只保留频率最高的 k 个元素,较小频率的会被不断淘汰。
最后一步是收集结果。此时堆中剩下的正是前 k 个高频元素,但顺序是从小到大(因为是小顶堆)。代码通过一个从后往前的循环不断 poll 堆顶,并把对应的 key(数字本身)放入结果数组 res 中。逆序填充可以让结果更自然地按频率从高到低排列。