代码随想录算法训练营第十三天|239. 滑动窗口最大值、347.前 K 个高频元素「栈与队列」

118 阅读5分钟

239. 滑动窗口最大值

题目

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。 返回 滑动窗口中的最大值

思路

只想到一个暴力解法,用两次循环,然后分别获得起始的滑动窗口和最大值,但是在输入特别大的案例的时候超出时间限制。

代码

自己写的错误代码

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number[]}
 */
var maxSlidingWindow = function(nums, k) {
    const res = [],tmp = []
    for (let i = 0;i<k;i++) {
        tmp.push(nums[i])
    }
    console.log(tmp)
    for (let i =0;i<nums.length-k+1;i++) {
        res.push(Math.max(...tmp))
        tmp.shift()
        tmp.push(nums[i+k])
    }
    return res
};

优化:

看了代码随想录的思路,没看明白,所以去看了视频看明白了思路,自己写了一下,单调队列能写出来,主函数里的添加逻辑有点迷糊,于是去看了大佬的写法,思路如下:

  1. 用两个指针分别指向数组的头尾
  2. 先对尾指针遍历,添加k个元素进单调队列中
  3. 然后对尾指针遍历,每次添加尾指针的元素进入单调,删除单调队列中头指针的元素,然后把当前滑动窗口的最大值添加进结果数组中。
  4. 最后返回结果数组。

代码

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number[]}
 */
var maxSlidingWindow = function(nums, k) {
    if (nums.length<=k) return [Math.max(...nums)]
    const res = [],queue = new myQueue()
    let i = 0,j=0
    while (i<k) {
        queue.myPush(nums[i++])
    }
    res.push(queue.myGetMax())
    while (i<nums.length) {
        queue.myPush(nums[i++])
        queue.myPop(nums[j++])
        res.push(queue.myGetMax())
    }
    return res
};

class myQueue {
    constructor() {
        this.queue = []
    }

    myPush = (x) => {
        while (this.queue[this.queue.length-1]<x){
            this.queue.pop()
        }
        this.queue.push(x)
    }

    myPop=(x)=>{
        if (this.queue.length&&this.queue[0]===x){
            this.queue.shift()
        }
    }

    myGetMax=()=> {
        return this.queue[0]
    }
}

总结:

这道题是做的第一个难度为困难的题,心里难免有点虚,可能会觉得自己肯定做不出来,还是尝试做了一下,暴力解法超时,去看了代码随想录的思路,最难的可能就是想出单调队列的方法,其实整个代码可以说不难,除了主函数那里有一点点难想到逻辑,有一点点绕,但是做完这道题以后,这个思路就是我的了。

347. 前 K 个高频元素

题目

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

思路

一开始想的就是用一个map遍历数组,key是元素,value是元素出现的次数,然后基于value对map排序,输出前k个。但是这个跟队列也没关系,代码我就没写,直接去看了代码随想录的思路。

代码

没写。

优化:

看了代码随想录的思路,感觉不太好理解,可能是当初二叉树没学好,堆啥的不太了解,看到优先级序列一脸懵,而且看完写不出来,去看了大佬的代码甚至也懵,只能一遍一遍捋,捋完以后自己敲了一遍,半卡不卡的写出来,中间停下来捋了好几次逻辑。 下面说一下思路:

  1. 首先定义一个优先级序列也就是一个堆
    1. 定义一个数组做队列,定义一个函数传入回调函数,后续用来定义堆的排列顺序。
    2. 定义堆的添加元素方法:
      1. 首先添加一个元素
      2. 然后声明两个指针,第一个指针index指向新添加的元素,第二个指针指向新添加元素的父元素。
      3. 声明一个循环,判断条件是只要父元素不小于0并且父元素的value大于新添加元素的value就执行:
        1. 把父元素和新添加元素换个位置
        2. 把父元素指针指向的位置赋值给index
        3. 重新找到index的父元素
    3. 定义堆的删除元素方法:
      1. 把堆顶元素拿到,最后用来返回,方便最后获取结果。
      2. 把队列末尾的元素取出放到堆顶位置
      3. 声明一个指针index指向堆顶,指针left指向左节点,指针child代表要操作的节点,指向左右节点中value更小的那个节点
      4. 声明一个循环,判断条件是child节点有含义并且index堆顶的value大于当前最小value的节点,执行:
        1. index和child节点的元素互换位置
        2. 把index重新指向child节点,此时child作为新的堆顶
        3. left指向index的左节点
        4. child再次指向左右节点中value较小的那个节点
    4. 定义一个size方法获取队列的长度
    5. 定义一个compare方法,设置了堆的排列顺序,利用传入的回调函数。同时判断如果相比较的节点中有一个不存在的情况。
  2. 然后是主函数,首先设置一个map,存入数组中出现的元素以及他们出现的次数,用键值对的形式保存
  3. 遍历map,把每个元素放入到堆中,超出k,就删除一个元素
  4. 声明一个res数组存放结果
  5. 倒序遍历堆,把最后的结果放入res数组中然后返回

代码

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number[]}
 */
var topKFrequent = function(nums, k) {
    const map = new Map()

    for (const 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.myPush(entry)

        if (heap.size()>k) {
            heap.myPop()
        }
    }
    const res = []
    for (let i = heap.size()-1;i>=0;i--) {
        res[i] = heap.myPop()[0]
    }
    return res
};

class Heap {
    constructor(compareFn) {
        this.compareFn = compareFn
        this.queue = []
    }

    myPush = (item) => {
        this.queue.push(item) 

        let index = this.queue.length - 1,
        parent = Math.floor((index-1)/2)

        while (parent>=0&&this.compare(parent,index)>0) {
            [this.queue[index],this.queue[parent]] = [this.queue[parent],this.queue[index]]

            index = parent
            parent= Math.floor((index-1)/2)
        }
    }

    myPop = () => {
        const out = this.queue[0]
        this.queue[0] = this.queue.pop()

        let index = 0,
        left = 1,
        child = this.compare(left,left+1)>0?left+1:left

        while (child!==undefined&&this.compare(index,child)>0) {
            [this.queue[index],this.queue[child]] = [this.queue[child],this.queue[index]]

            index = child
            left = 2*index + 1
            child = this.compare(left,left+1)>0?left+1:left
        }
        return out
    }

    size = () => {
        return this.queue.length
    }

    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])
    }
}

总结:

这道题我感觉比上道题还难,感觉是十二天以来做的最难的一道题,可能是因为对堆这个数据结构的不熟悉,感觉特别难理解,看了答案代码都要一遍一遍的捋逻辑,遇到卡壳的地方,就多代入几个用例,一遍一遍的顺,大概弄清楚以后,写了代码。 其实在写这篇博客之前,对于删除操作中,left左节点的取值还是有点模糊,可能是因为对堆不熟悉,然后硬着头皮写了一遍思路以后,豁然开朗了,收获很多。

Day13总结

今天的题目还是挺难的,前天做的栈的题目以为队列一样简单,但是今天的两题思路都挺难想的,也是做了那么多天题目感觉最难的一道,希望二刷的时候能见证自己的成果。