[路飞]滑动窗口最大值

119 阅读1分钟

记录 1 道算法题

滑动窗口的最大值

leetcode-cn.com/problems/sl…


给一个数组,然后划出一个区间,这个区间从下标 0 开始,每次往右移动 1。然后将每次区间的最大值返回。

nums = [1,3,-1,-3,5,3,6,7], k = 3

结果 [3,3,5,5,6,7]

维护一个区间的数字,得到最大值,很适合用堆解决。首先将第一个区间 [0,k] 的数存入堆中,然后每移动一位就推一个新的数字进入堆中。但是我们不能确定有哪些数是已经随着移动不再是处于区间的范围内。所以我们存入堆中的得是一个对象,包括数字和他所在的下标。堆只关心堆顶的元素,所以只要堆顶的元素离开了区间,就弹出。直到堆顶的元素处于区间内。

    function maxSlidingWindow(nums, k) {
        // 自己实现了一个堆,这里创建实例。
        // 最大值所以倒序排列
        const heap = new Heap((a, b) => b[0] - a[0])
        // 准备第一个区间
        for(let i = 0; i < k; i++) {
            heap.push([nums[i], i])
        }
        
        // 开始移动
        const result = [heap.data[0][0]]
        for(let i = k; i < nums.length - k + 1; i++) {
            heap.push([nums[i + k - 1], i + k - 1])
            // 比较堆顶元素是否合格
            
            while(true) {
                const [num, index] = heap.data[0]
                if (index >= i && index <= i + k - 1) {
                    result.push(num)
                    break
                } else {
                    heap.pop()
                }
            }
        }
        
        return result
    }
  1. 单调队列

思路和上面的堆是一样的,通过维护一个队列,队列里倒序排列,第一个是当前队列最大的数。当移动时队列加入新的数,由于是倒序排列,这个数会冒泡到队列的头部。单调队列的特性是按照升序或者降序排列,然后前面的数字存在,则说明后面的数字一定比前面小或者大。利用这个特点,每次推入一个数的时候,我们可以将队列中比这个数小的数都弹出。保证是单调的。

为了保证队列的第一个数是在区间内的,所以需要从前面删除不在区间的数字。

    function maxSlidingWindow(nums, k) {
        const len = nums.length
        const queue = []
        
        for(let i = 0; i < k; i++) {
            // 将比这个数小的数都弹出
            // 一定要 >= 因为存下标的关系,所以最好保持最新的,
            // 不然就会有很多从前面删除不在区间内的数
            while(queue.length && nums[i] >= queue[queue.length - 1]) {
                queue.pop()
            }
            queue.push(i)
        }
        
        const result = [nums[queue[0]]]
        for(let i = k; i < len; i++) {
            // 剩下的数也要加入队列,同样 >=
            while(queue.length && nums[i] >= queue[queue.length - 1]) {
                queue.pop()
            }
            queue.push(i)
            
            // 保证数都在区间内
            // i 是区间的结尾
            while(queue[0] < i - k) {
                queue.shift()
            }
            
            result.push(nums[queue[0]])
        }
    }
  1. 还有一个方法

思想是将按照区间分块。 [][][]

然后将分块分为从左到右升序的数组 a 和从右到左升序的两个数组 b,然后两个交叉,让 b 作为区间的开始,让 a 作为区间的结束。区间就会变成两边高中间低的情况,于是只要比较区间的开头和结尾两个数谁大就可以了。

然后在区间的倍数的地方用自己的值。

nums = [1,3,-1,-3,5,3,6,7], k = 3

数组 a 则是这个样子
--> 
[1, 3, 3, -3, 5, 5, 6, 7]

数组 b 则是这个样子
<--
[3, 3, -1, 5, 5, 3, 7, 7]

比较:Math.max(b[i], a[i+k-1])
    function maxSlidingWindow(nums, k) {
        const len = nums.length
        const a = new Array(len).fill(0)
        const b = new Array(len).fill(0)
        
        for(let i = 0; i < len; i++) {
            if (i % k === 0) {
                // 分块的位置
                a[i] = nums[i]
            } else {
                a[i] = Math.max(a[i - 1], nums[i])
            }
        }
        
        for(let i = len - 1; i >= 0; i--) {
            // 第一个数和分块区间的结尾
            if (i === len - 1 && (i + 1) % k === 0) {
                b[i] = nums[i]
            } else {
                b[i] = Math.max(b[i + 1], nums[i])
            }
        }
        
        const result = []
        // 区间的起点
        for(let i = 0; i <= len - k; i++) {
            result.push(Math.max(b[i], a[i + k - 1]))
        }
        
        return result
    }

结束