2023.3.27 滑动窗口的最大值
方式一 直接记录值
直观能想到的思路,根据滑动窗口的大小去循环遍历数组,随后在滑动窗口里面再次遍历寻找最大值。
但是这样做会导致时间复杂度过高。
考虑使用于一个队列去维护滑动窗口里面的值,为了找到滑动窗口的最大值,我们可以构造一个单调递减的队列,这样要找到每次滑动窗口的最大值时,我们只需要peek()队列头的元素即可。那么为了构造这样一个单调递减的队列,我们对每次push进来的元素进行特殊的处理,当当前队列为空时,直接push,否则,当加入的元素大于队列尾的元素时,删除队列尾的元素,直到队列尾元素大于待加入元素或者队列尾空,然后push。
当滑动窗口移动的时候,我们需要pop掉数组的元素,显然,当待删除的数组元素小于滑动窗口的最大值时,它在这个滑动窗口的最大值加入的时候已经被移除掉了,此时无需在队列中进行pop操作,直到我们需要pop的元素就是当前滑动窗口的最大值时,他就是我们队列第一个元素,此时才需要执行pop操作。随后对待加入的元素执行push操作,遍历整个数组即可。此时时间复杂度就是整个数组的长度O(n)
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
MyQueue myQueue = new MyQueue();
//储存结果的数组
int[] res = new int[nums.length - k + 1];
int index = 0;
//首先将nums数组前k个计入队列中
for(int i = 0;i<k;i++){
myQueue.offer(nums[i]);
}
//此时myQueue的队列头就是第一个滑动窗口的最大值
res[index++] = myQueue.peek();
//随后要开始循环移动滑动窗口,执行poll和offer操作
for(int i = k;i<nums.length;i++){
//删除滑动窗口第一个元素
myQueue.poll(nums[i-k]);
//增加后面一个元素
myQueue.offer(nums[i]);
//获取最大值
res[index++] = myQueue.peek();
}
return res;
}
}
class MyQueue{
Deque<Integer> deque = new LinkedList<>();
void offer(int value){
//加入元素时,需要检查队列尾部的元素与value的大小关系,构造单调递减的队列。
while(!deque.isEmpty() && deque.getLast() < value){
deque.removeLast();
}
//结束循环,要么value比所有元素都大,此时队列为空,直接加入,要么遇到了比它更大的,也直接加入即可。
deque.offer(value);
}
void poll(int value){
//执行poll操作时,我们需要比较当前待移除数组元素的值是否在队列里面,如果没在,说明这个元素值后面存在当前滑动窗口最大值,在这个最大值加入队列时将其删除了,此时不需要执行pop操作,如果在队列里面,那么一定是队列头部,即待删除元素为滑动窗口最大值,此时执行pop操作。
if(!deque.isEmpty() && deque.peek() == value ){
deque.poll();
}
}
int peek(){
return deque.peek();
}
}
方式二 使用数组下标的方式来记录
相比于记录数组值,记录数组下标的好处在于我们可以通过判断滑动窗口所处范围来决定是否需要移除队列元素
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
Deque<Integer> deque = new LinkedList<>();
int[] res = new int[nums.length-k+1];
int index = 0;
for(int i = 0;i<nums.length;i++){
//同样地,执行添加元素的操作时,我们需要构造单调递减的队列,方便直接取出最大值
while(!deque.isEmpty() && nums[deque.peekLast()] < nums[i]){
deque.removeLast();
}
//储存下标
deque.offer(i);
//当遍历到超过窗口长度时,我们需要开始执行poll操作
//如果我们最大值的下标刚好是滑动窗口移动前的第一个值,那么就需要将其移除。
if(i> k - 1 && deque.peek()< (i - k + 1)){
deque.pop();
}
if(i >= k-1){//构成了一个滑动窗口,开始记录最大值
res[index++] = nums[deque.peek()];
}
}
return res;
}
}
2023.3.27 前K个高频元素
首先,我们要统计前K个高频元素,必须要做的就是需要统计每个元素出现的次数,这一点我们可以直观的想到使用map来统计。
统计之后,如何对频率进行排序处理?实际上,我们可以使用优先级队列来处理,优先级队列本质上是使用堆结构来进行实现的,存在两种结构:
大顶堆,小顶堆。其中堆是一个完全二叉树,如果树中的父节点是大于左右孩子的,我们称之为大顶堆,反之,我们称之为小顶堆。
那么如何选择使用哪一种顶堆呢?
假设一下,如果我们使用大顶堆,先构造了一个大小为k的大顶堆,当更新大顶堆时,会把当前最大的元素弹出,这样就会损失数据,因此不可以。
那为什么小顶堆可以呢?因为当我们维护一个大小为k的小顶堆时,每次更新时,我们就会把最小的元素弹出,最后只剩下了k个最大的,就是我们要求的元素。
class Solution {
public int[] topKFrequent(int[] nums, int k) {
//首先构建一个map来记录每个元素出现的频率
Map<Integer,Integer> map = new HashMap<>();
for(int x : nums){
map.put(x,map.getOrDefault(x,0) + 1);
}
//将频率记录好之后,构建一个优先队列,存放一个int类型的数组,分别存放元素和出现频率
//实现compare
PriorityQueue<int[]> queue = new PriorityQueue<>(new Comparator<int[]>(){
//实现小顶堆的方法
public int compare(int[] m,int[] n){
return m[1] - n[1];
}
});
//遍历map中的key和value,使用entrySet的getKey和getValue方法,放入优先队列中
for(Map.Entry<Integer,Integer> entry : map.entrySet()){
if(queue.size() < k){//小顶堆不到K个,直接添加元素
queue.offer(new int[]{entry.getKey(),entry.getValue()});
}
else{//当小顶堆已经记录了k个元素,要想更新小顶堆,我们需要判断堆顶的元素与当前元素的大小。
if(queue.peek()[1]<entry.getValue()){
queue.poll();
queue.offer(new int[]{entry.getKey(),entry.getValue()});
}
}
}
//结束循环,得到的优先队列此时存放的数值就是我们前k个高频元素和对应出现的频率
int[] res = new int[k];
for(int i = k-1;i>=0;i--){
res[i] = queue.poll()[0];
}
return res;
}
}
涉及到的相关知识:
优先队列。
排序方式的compare实现
map的遍历方式,entrySet实现,可以使用getKey和getValue方法