代码随想录算法训练营Day 13|239. 滑动窗口最大值、347. 前 K 个高频元素

82 阅读5分钟

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操作要保持如下规则:

  1. pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作
  2. 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 高的元素。你可以按 任意顺序 返回答案。

思路

  1. 要统计元素出现频率 —— map
  2. 对频率排序 —— 最大堆/最小堆
  3. 找出前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
};