Leetcode 239. 滑动窗口最大值
思路
根据滑动窗口的移动过程,每次移动时,很像是一个队列。每次移动完成三步操作:pop( ), push( ), getMaxValue( )。
如果用优先级队列来解决这道题,由于我们需要获取最大值,那采用大顶堆(排序的二叉树,将最大值放在最上面)。每次将窗口中的数递减存入队列中,那么再次向后移动时,由于原来的数字顺序发生改变,我们可能无法直接pop掉原先的数值。所以不能使用优先级队列。
这里我们使用单调队列,即维护元素里面的单调递增和单调递减,和优先级队列不同,我们可以自定义元素的添加、pop、push。对于本道题,我们没有必要将滑动窗口内的所有数都加入队列,只需要维护可能成为最大值的序列。
难点
设计单调队列的时候,pop,和push操作要保持如下规则:
- pop(value):如果窗口移除的元素==单调队列的出口元素,那么队列弹出元素,否则不用任何操作。
- push(value):如果窗口添加的元素>单调队列入口的元素,那么就将队列入口的元素弹出,直到push元素的数值<=队列入口元素的数值为止。
保持如上规则,每次窗口移动的时候,只要deque.front()就可以返回当前窗口的最大值。
为了更直观的感受到单调队列的工作过程,以题目示例为例,输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3,动画如下:
代码(Java)
写法:自定义单调队列
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 index = 0;
MyQueue myQueue = new MyQueue();
//先将前k个元素放入队列
for (int i = 0; i < k; i++) {
myQueue.add(nums[i]);
}
res[index++]=myQueue.peek();
//滑动窗口
for (int i = k; i < nums.length; i++) {
myQueue.poll(nums[i-k]);
myQueue.add(nums[i]);
res[index++]=myQueue.peek();
}
return res;
}
}
代码随想录链接
下面是代码随想录的文章链接与视频链接,帮助大家更好地理解这道题。
文章
视频
Leetcode 347. 前 K 个高频元素
思路
这道题需要做下列三个操作:
- 统计各元素出现的频率
- 对频率进行排序
- 找出前k个高频元素
由于需要统计各个元素出现的频率,根据之前所学的哈希,我们可以用HashMap来进行统计。key存放元素,value存放元素出现的次数。
接着我们再按照value进行排序,按快速排序的时间复杂度,对所有元素进行排序为nlogn。但其实没有必要对所有元素的频率进行排序,我们只需要k个高频元素的有序集合,所以用到大顶堆(堆头是最大元素)和小顶堆(堆头是最小元素)。
难点
这道题我一开始认为采用大顶堆,因为需要求前k个高频元素,则有序集合里面的元素从大到小来排序,所以用大顶堆。若定义一个大小为k的大顶堆,当我们添加新的元素时,却pop掉了最大的元素。
所以我们要用小顶堆,因为只有小顶堆pop掉最小的元素,最后小顶堆里积累的才是前k个最大元素。
寻找前k个最大元素流程如图所示:(图中的频率只有三个,所以正好构成一个大小为3的小顶堆,如果频率更多一些,则用这个小顶堆进行扫描)
代码(Java)
写法:小顶堆
时间复杂度:O(nlogn)
空间复杂度:O(n)
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<int[]> priorityQueue = new PriorityQueue<>((pair1,pair2)->pair1[1]-pair2[1]);
for (Map.Entry<Integer,Integer> entry : map.entrySet()){
if (priorityQueue.size()<k){
priorityQueue.add(new int[]{entry.getKey(), entry.getValue()});
}else {
//当前元素出现次数大于小顶堆的根结点
if (entry.getValue()>priorityQueue.peek()[1]){
priorityQueue.poll();
priorityQueue.add(new int[]{entry.getKey(),entry.getValue()});
}
}
}
int[] res = new int[k];
for (int i = k-1; i >= 0 ; i--) {
res[i] = priorityQueue.poll()[0];
}
return res;
}
}
代码随想录链接
下面是代码随想录的文章链接与视频链接,帮助大家更好地理解这道题。
文章
视频
总结
在239. 滑动窗口最大值中,由于队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的,这里使用了自定义单调队列。
在347. 前 K 个高频元素中,使用了优先级队列,这里我们也是对部分数据进行排序,而不是对所有数据排序。大家经常说的大顶堆(堆头是最大元素),小顶堆(堆头是最小元素),如果懒得自己实现的话,就直接**用PriorityQueue(优先级队列)**就可以了,底层实现都是一样的,从小到大排就是小顶堆,从大到小排就是大顶堆。