单调队列刷题总结

257 阅读1分钟

题目介绍

题目链接:leetcode.cn/problems/sl…

image.png

接下来,我们就借助单调队列结构,用O(1)时间算出每个滑动窗口中的最大值,使得整个算法在线性时间完成。

分析

在介绍「单调队列」这种数据结构的 API 之前,先来看看一个普通的队列的标准 API:

class Queue {
    // enqueue 操作,在队尾加入元素 n
    void push(int n);
    // dequeue 操作,删除队头元素
    void pop();
}

我们要实现的「单调队列」的 API 也差不多:

class MonotonicQueue {
    // 在队尾添加元素 n
    void push(int n);
    // 返回当前队列中的最大值
    int max();
    // 队头元素如果是 n,删除它
    void pop(int n);
}

当然,这几个 API 的实现方法肯定跟一般的 Queue 不一样,不过我们暂且不管,而且认为这几个操作的时间复杂度都是 O(1),先把这道「滑动窗口」问题的解答框架搭出来:

public int[] maxSlidingWindow(int[] nums, int k) {
    MonotonicQueue window = new MonotonicQueue();
    List<Integer> res = new ArrayList<>();
    for(int i = 0; i < nums.length; i++) {
        if(i < k-1) {
            //先填满窗口的前k-1个元素
            window.push(nums[i]);
        } else {
            //窗口向前滑动,加入新数字
            window.push(nums[i]);
            //记录当前窗口的最大值
            res.add(window.max());
            //移除旧的数字
            window.pop(nums[i-k+1]);
        }
    }
    //需要转成int[]数组再返回
    int[] arr = new int[res.size()];
    for(int i = 0; i < res.size(); i++) {
        arr[i] = res.get(i);
    }
    return arr;
}

image.png

这个思路很简单,能理解吧?下面我们开始重头戏,单调队列的实现。

观察滑动窗口的过程就能发现,实现「单调队列」必须使用一种数据结构支持在头部和尾部进行插入和删除,很明显双链表是满足这个条件的。

「单调队列」的核心思路和「单调栈」类似,push方法依然在队尾添加元素,但是要把前面比自己小的元素都删掉:

/**
 * 单调队列的实现
 */
class MonotonicQueue {
    //双向链表,支持头部和尾部增删元素
    //维护其中的元素自尾部到头单调递增
    LinkedList<Integer> queue = new LinkedList<>();

    /**
     * 往单调队列中添加元素值n
     */
    public void push(int n) {
        //将小于n的元素全部删除
        while(!queue.isEmpty() && queue.getLast() < n) {
            queue.pollLast();
        }
        //然后将n加入到队尾
        queue.addLast(n);
    }
}    

你可以想象,加入数字的大小代表人的体重,把前面体重不足的都压扁了,直到遇到更大的量级才停住。

image.png

如果每个元素被加入时都这样操作,最终单调队列中的元素大小就会保持一个单调递减的顺序,因此我们的max方法可以可以这样写:

/**
 * 获取单调队列中元素最大值
 */
public int max() {
    return queue.getFirst();
}

pop方法在队头删除元素n,也很好写:

/**
 * 从单调队列中移除元素值n
 */ 
public void pop(int n) {
    if(n == queue.getFirst()) {
        queue.pollFirst();
    }
} 

之所以要判断data.getFirst() == n,是因为我们想删除的队头元素n可能已经被「压扁」了,可能已经不存在了,所以这时候就不用删除了:

image.png

完整代码如下:

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        MonotonicQueue window = new MonotonicQueue();
        List<Integer> res = new ArrayList<>();
        for(int i = 0; i < nums.length; i++) {
            if(i < k-1) {
                //先填满窗口的前k-1个元素
                window.push(nums[i]);
            } else {
                //窗口向前滑动,加入新数字
                window.push(nums[i]);
                //记录当前窗口的最大值
                res.add(window.max());
                //移除旧的数字
                window.pop(nums[i-k+1]);
            }
        }
        //需要转成int[]数组再返回
        int[] arr = new int[res.size()];
        for(int i = 0; i < res.size(); i++) {
            arr[i] = res.get(i);
        }
        return arr;
    }
}

/**
 * 单调队列的实现
 */
class MonotonicQueue {
    LinkedList<Integer> queue = new LinkedList<>();

    /**
     * 往单调队列中添加元素值n
     */
    public void push(int n) {
        //将小于n的元素全部删除
        while(!queue.isEmpty() && queue.getLast() < n) {
            queue.pollLast();
        }
        //然后将n加入到队尾
        queue.addLast(n);
    } 

    /**
     * 获取单调队列中元素最大值
     */
    public int max() {
        return queue.getFirst();
    }

    /**
     * 从单调队列中移除元素值n
     */ 
    public void pop(int n) {
        if(n == queue.getFirst()) {
            queue.pollFirst();
        }
    } 
}