LeetCode - 239 滑动窗口最大值

目录

题目来源

题目描述

示例

提示

题目解析

算法源码


题目来源

239. 滑动窗口最大值 - 力扣(LeetCode)

题目描述

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

扫描二维码关注公众号,回复: 14945956 查看本文章

返回 滑动窗口中的最大值 。

示例

输入 nums = [1,3,-1,-3,5,3,6,7], k = 3
输出 [3,3,5,5,6,7]
说明 滑动窗口的位置           最大值
---------------                     -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7
 
输入 nums = [1], k = 1
输出 [1]
说明

提示

  • 1 <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4
  • 1 <= k <= nums.length

题目解析

本题最简单的思路其实就是定义一个长度为k的滑窗,然后每次求滑窗范围内最大值,但是这样的算法的时间复杂度为O((n - k + 1) * k),因此算法的性能比较差。

本题最佳解题思路是:单调队列。

单调队列在此处是用于维护滑窗的最大值的。

如下图是用例1的滑窗运动过程,和单调队列的变化

滑窗运动过程分为两块:

  • 初始滑窗的形成过程(即只有新增尾部元素的过程)
  • 滑窗的右移过程(即失去一个头部(绿色)元素,新增一个尾部元素)

我们假设滑窗的尾巴元素是tail,而新加入滑窗的元素是new,那么为了维护单调队列的单调性,本题是单调递减,只要滑窗的tail < new,那么就必须将tail出队,然后继续比较滑窗的新tail和new,直到滑窗的tail >= new了,或者滑窗为空了,此时将new加入到滑窗尾部。

上面逻辑对应单调队列的尾删操作、以及尾增操作。

另外,单调队列还有一个非常重要的头删操作。比如上图中如下两个过程

新滑窗失去了3元素,新增了5元素,那么其实此时单调队列[3, -1, -3]按顺序需要做如下两件事:

  • 先删除队列的头元素3,此时单调队列变为[-1, -3]
  • 然后再加入5到队列,此时单调队列变为[-1, -3, 5],为了维护单调性,因此依次尾删-3、-1,最后单调队列就只有[5]

从这个过程,我们发现单调队列中3元素,并不是为了维护单调性而被尾删的,而是被头删的。

为什么呢?

从上图两个黄色的滑窗,我们可以发现,滑窗移动过程中,失去了3元素,因此新滑窗需要删掉3元素。

这里我们可以对比下面过程来分析

上图中新滑窗失去了1元素,但是单调队列[3, -1]却没有执行头删,这是因为单调队列的头部元素3还在新滑窗中,因此不需要头删。

因此,只有新滑窗失去的元素 == 单调队列的头部元素 时,我们才需要进行单调队列的头删操作。 

Java算法源码

class Solution {
  public int[] maxSlidingWindow(int[] nums, int k) {
    // queue 是单调队列
    LinkedList<Integer> queue = new LinkedList<>();

    // ans 记录题解,一共有nums.length - k + 1滑窗
    int[] ans = new int[nums.length - k + 1];
    // j 记录当前滑窗的序号
    int j = 0;

    // 初始滑窗
    for (int i = 0; i < k; i++) {
      while (queue.size() > 0 && queue.getLast() < nums[i]) {
        queue.removeLast();
      }
      queue.add(nums[i]);
    }
    ans[j++] = queue.getFirst();

    // 后续滑窗
    for (int i = k; i < nums.length; i++) {
      // nums[i-k] 是滑窗失去的元素
      if (nums[i - k] == queue.getFirst()) {
        queue.removeFirst(); // 单调队列头删
      }

      // nums[i] 是滑窗新增的元素
      while (queue.size() > 0 && queue.getLast() < nums[i]) {
        queue.removeLast(); // 单调队列尾删
      }

      queue.add(nums[i]); // 单调队列尾增
      ans[j++] = queue.getFirst();
    }

    return ans;
  }
}

JavaScript算法源码

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number[]}
 */
var maxSlidingWindow = function(nums, k) {
    const queue = [];
    const ans = [];

    for(let i=0; i<k; i++) {
        while(queue.length && queue.at(-1) < nums[i]) {
            queue.pop();
        }
        queue.push(nums[i]);
    }
    ans.push(queue[0]);

    for(let i=k; i<nums.length; i++) {
        if(nums[i-k] == queue[0]) {
            queue.shift();
        }

        while(queue.length && queue.at(-1) < nums[i]) {
            queue.pop();
        }
        queue.push(nums[i]);
        ans.push(queue[0]);
    }
    return ans;
};

上面JS使用的数组模拟的双端队列,因此shift()操作的性能非常差,下面代码中,我模拟了一个双端队列MyQueue,包含头部出队shift,尾部出队pop,尾部入队push,以及获取双端队列头部first和尾部值last。

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number[]}
 */
var maxSlidingWindow = function(nums, k) {
    const queue = new MyQueue();
    const ans = [];

    for(let i=0; i<k; i++) {
        while(queue.length && queue.last() < nums[i]) {
            queue.pop();
        }
        queue.push(nums[i]);
    }
    ans.push(queue.first());

    for(let i=k; i<nums.length; i++) {
        if(nums[i-k] == queue.first()) {
            queue.shift();
        }

        while(queue.length && queue.last() < nums[i]) {
            queue.pop();
        }
        queue.push(nums[i]);
        ans.push(queue.first());
    }
    return ans;
};

class MyQueue{
    constructor() {
        this.head = null;
        this.tail = null;
        this.length = 0;
    }

    push(val) {
        const node = new Node(val);

        if(this.length == 0) {
            this.head = node;
            this.tail = node;
        } else {
            this.tail.next = node
            node.prev = this.tail
            this.tail = node
        }

        this.length += 1;
    }

    pop() {
        if(this.length > 0) {
            this.tail = this.tail.prev;
            if(this.tail) this.tail.next = null;
            this.length -= 1;
        }
    }

    shift() {
        if(this.length > 0) {
            this.head = this.head.next;
            if(this.head) this.head.prev = null;
            this.length -= 1;
        }
    }

    last() {
        return this.tail.val;
    }

    first() {
        return this.head.val;
    }
}

class Node {
    constructor(val) {
        this.val = val;
        this.prev = null;
        this.next = null;
    }
}

Python算法源码

from collections import deque


class Solution(object):
    def maxSlidingWindow(self, nums, k):
        """
        :type nums: List[int]
        :type k: int
        :rtype: List[int]
        """

        # dq 是单调队列
        dq = deque()
        # ans 记录题解,一共有nums.length - k + 1滑窗
        ans = []

        # 初始滑窗
        for i in range(k):
            while len(dq) > 0 and dq[-1] < nums[i]:
                dq.pop()
            dq.append(nums[i])
        ans.append(dq[0])

        # 后续滑窗
        for i in range(k, len(nums)):
            # nums[i-k] 是滑窗失去的元素
            if nums[i - k] == dq[0]:
                dq.popleft()  # 单调队列头删

            # nums[i] 是滑窗新增的元素
            while len(dq) > 0 and dq[-1] < nums[i]:
                dq.pop()  # 单调队列尾删

            dq.append(nums[i])  # 单调队列尾增
            ans.append(dq[0])

        return ans

 

猜你喜欢

转载自blog.csdn.net/qfc_128220/article/details/130474096