239. 滑动窗口最大值
要求: 给你一个整数数组 nums,有一个大小为 k **的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
思路
本题一开始考虑暴力方法,遍历一遍每次从窗口中再找到最大值,但明显超时。此时我们需要一个队列,队列里放进去窗口里的元素,然后随着窗口的移动,队列也一进一出,每次移动之后,队列告诉我们里面的最大值是什么。
class MyQueue {
public:
void pop(int value) {
}
void push(int value) {
}
int front() {
return que.front();
}
};
每次窗口移动的时候,调用que.pop(滑动窗口中移除元素的数值),que.push(滑动窗口添加元素的数值),然后que.front()就返回我们要的最大值。
因此,队列里的元素很明显是需要排序的,需要将最大值放在出对口,但如何保证每次队列最前面的是最大值呢?
队列中不需要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以,保证队列里的元素值是由大到小的,这就是单调队列。
设计单调队列的时候,pop,和push操作要保持如下规则:
- pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作
- push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止
保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。
但不知道什么情况,我用单调队列一直超时。
class MonoQueue {
constructor(){
this.queue = []
}
popQue(value){
// 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出
// 同时pop之前判断队列当前是否为空
if(this.queue.length != 0 && value == this.queue[0]){
this.queue.shift()
}
}
pushQue(value){
// 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。
// 这样就保持了队列里的数值是单调从大到小的了。
while(this.queue.length != 0 && value > this.queue[this.queue.length-1]){
this.queue.pop()
}
this.queue.push(value)
}
frontQue(){
return this.queue[0]
}
}
var maxSlidingWindow = function(nums, k) {
//使用deque来实现单调队列
let que = new MonoQueue()
let res = []
let i=0, j=0
while(j<k){
que.pushQue(nums[j++])
}
res.push(que.frontQue())
while(j<nums.length){
que.pushQue(nums[j]) //移除滑动窗口最前面的元素
que.popQue(nums[i])
res.push(que.frontQue())
i++
j++
}
return res
};
队列里存放的是索引值
var maxSlidingWindow = function(nums, k) {
//使用deque来实现单调递减队列
let que = [] //存放的是索引
let res = []
for(let i=0; i<nums.length; i++){
//入
while(que.length != 0 && nums[i] >= nums[que[que.length-1]]){
//单调递减,如果que中的最后一个比将要放入的数小,则删掉
que.pop()
}
que.push(i)
//出
while(que[0]<i-k+1){
//队首超出滑动窗口,移除队首元素
que.shift()
}
if(i >=k-1){
// 由于队首到队尾单调递减,所以窗口最大值就是队首
res.push(nums[que[0]])
}
}
return res
};
347. 前 K 个高频元素
时间复杂度 O(nlogk) 空间复杂度 O(1)
要求:给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
思路
- 要统计元素出现频率 —— map
- 对频率排序 —— 最大堆/最小堆
- 找出前K个高频元素
使用小顶堆呢,还是大顶堆?大顶堆是堆头是最大的元素,小顶堆堆头是最小的元素。
题目要求前 K 个高频元素,如果定义一个大小为k的大顶堆,在每次移动更新大顶堆的时候,每次弹出都把最大的元素弹出去了,无法保留来前K个高频元素。
所以本题要用小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。
最小堆有两个方法:插入和移出。
插入:将元素放置在堆的末尾,递归地将它与父节点元素对比,如果其值比父节点小就交换位置,直至其值大于等于父节点或移至到堆的首部。
移出:堆中最小的值位于堆顶,先将其与堆尾元素交换位置,移出堆尾元素;将堆顶元素递归地与左右子节点元素比较,如果其值大于子节点就交换位置,直至其值小于等于子节点。
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
var topKFrequent = function(nums, k) {
class Heap {
constructor(compareFn){
this.compareFn = compareFn
this.queue = []
}
push(item){
this.queue.push(item)
//上浮
let index = this.size()-1 //记录推入元素下标
let parent = Math.floor((index-1)/2) //记录父节点下标
while(parent >= 0 && this.compare(parent, index) > 0){
this.swap(index, parent)
//更新下标
index = parent
parent = Math.floor((index-1)/2)
}
}
pop(){
if(this.size() == 0) return
let out = this.queue[0] // 获取堆顶元素
this.queue[0] = this.queue.pop() //移除堆顶元素,将最后一个元素填入
//下沉
let index =0
let left = index*2 +1
//选择左右子树中较小的
let searchChild = this.compare(left, left+1) >0 ? left+1 : left
while(searchChild != undefined && this.compare(index, searchChild)>0){
this.swap(index, searchChild)
//更新下标
index = searchChild
left = index*2 +1
searchChild = this.compare(left, left+1)>0 ? left+1 : left
}
return out
}
size(){
return this.queue.length
}
swap(i, j){
[this.queue[i], this.queue[j]] = [this.queue[j], this.queue[i]]
}
compare(index1, index2){
if(this.queue[index1] === undefined) return 1
if(this.queue[index2] === undefined) return -1
return this.compareFn(this.queue[index1], this.queue[index2])
}
}
const map = new Map()
for(let num of nums){
map.set(num, (map.get(num) || 0)+1)
}
//创建小顶堆
const heap = new Heap((a,b)=>a[1]-b[1])
for(const entry of map.entries()){
heap.push(entry)
if(heap.size()>k){
heap.pop()
}
}
console.log(heap)
let res = []
for(let i=heap.size()-1; i>=0; i--){
res.push(heap.pop()[0])
//注意heap.size()是变化的
}
return res
};