掘金 后端 ( ) • 2024-06-07 14:15

单调队列-滑动窗口

题目

给定一个大小为 n≤10^6^ 的数组。

有一个大小为 k 的滑动窗口,它从数组的最左边移动到最右边。

你只能在窗口中看到 k 个数字。

每次滑动窗口向右移动一个位置。

以下是一个例子:

该数组为 [1 3 -1 -3 5 3 6 7],k 为 3 。

窗口位置 最小值 最大值 [1 3 -1] -3 5 3 6 7 -1 3 1 [3 -1 -3] 5 3 6 7 -3 3 1 3 [-1 -3 5] 3 6 7 -3 5 1 3 -1 [-3 5 3] 6 7 -3 5 1 3 -1 -3 [5 3 6] 7 3 6 1 3 -1 -3 5 [3 6 7] 3 7

你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。

输入格式 输入包含两行。

第一行包含两个整数 n 和 k ,分别代表数组长度和滑动窗口的长度。

第二行有 n 个整数,代表数组的具体数值。

同行数据之间用空格隔开。

输出格式 输出包含两个。

第一行输出,从左至右,每个位置滑动窗口中的最小值。

第二行输出,从左至右,每个位置滑动窗口中的最大值。

输入样例: 8 3 1 3 -1 -3 5 3 6 7 输出样例: -1 -3 -3 -3 3 3 3 3 5 5 6 7

题目中提到了滑动窗口,我们先以示例为例看下什么是滑动窗口?在示例中,我们从数组中第一个元素开始遍历,由于窗口的大小是3,因此当遍历到第三个元素时,窗口就形成了。

image.png

之后,继续遍历元素时,为了保持窗口的大小为3,左侧元素就需要从窗口中剔除。这样使得窗口一直在向右移动,直到考察到最后一个元素结束,这就是所谓的滑动窗口。

image.png

解题思路(以最大值为例):

由于我们需要求出的是滑动窗口的最大值。

  1. 如果当前的滑动窗口中有两个下标 ij ,其中ij的左侧(i < j),并且i对应的元素不大于j对应的元素(nums[i] ≤ nums[j]),则:

  2. 当滑动窗口向右移动时,只要 i 还在窗口中,那么 j 一定也还在窗口中。这是由于 ij 的左侧所保证的。

因此,由于 nums[j] 的存在,nums[i] 一定不会是滑动窗口中的最大值了,我们可以将nums[i]永久地移除。

  1. 因此我们可以使用一个队列存储所有还没有被移除的下标。在队列中,这些下标按照从小到大的顺序被存储,并且它们在数组nums中对应的值是严格单调递减的。

  2. 当滑动窗口向右移动时,我们需要把一个新的元素放入队列中。

  3. 为了保持队列的性质,我们会不断地将新的元素与队尾的元素相比较,如果新元素大于等于队尾元素,那么队尾的元素就可以被永久地移除,我们将其弹出队列。我们需要不断地进行此项操作,直到队列为空或者新的元素小于队尾的元素。

  4. 由于队列中下标对应的元素是严格单调递减的,因此此时队首下标对应的元素就是滑动窗口中的最大值。

  5. 窗口向右移动的时候。因此我们还需要不断从队首弹出元素保证队列中的所有元素都是窗口中的,因此当队头元素在窗口的左边的时候,弹出队头。

总结

最小值和最大值分开来做,两个for循环完全类似,都做以下四步

  1. 解决队首已经出窗口的问题;
  2. 解决队尾与当前元素a[i]不满足单调性的问题;
  3. 将当前元素下标加入队尾;
  4. 如果满足条件则输出结果;

需要注意的细节:

  1. 上面四个步骤中一定要先3后4,因为有可能输出的正是新加入的那个元素;
  2. 队列中存的是原数组的下标,取值时要再套一层,a[q[]];
  3. 算最大值前注意将hh和tt重置;
  4. 此题用cout会超时,只能用printf;
  5. hh从0开始,数组下标也要从0开始。

代码:

#include <iostream>
#include <cstring>
#include <algorithm>
#include <deque>
using namespace std;

const int N = 1000010;
int a[N];
int main()
{
    int n, k;
    cin >> n >> k;
    for (int i = 1; i <= n; i ++ ) cin >> a[i];//读入数据
    deque<int> q;
    for(int i = 1; i <= n; i++)
    {
        while(q.size() && q.back() > a[i]) //新进入窗口的值小于队尾元素,则队尾出队列
            q.pop_back();
        q.push_back(a[i]);//将新进入的元素入队
        if(i - k >= 1 && q.front() == a[i - k])//若队头是否滑出了窗口,队头出队 
            q.pop_front();
        if(i >= k)//当窗口形成,输出队头对应的值
            cout << q.front() <<" ";
    }
    q.clear();
    cout << endl;

    //最大值亦然
    for(int i = 1; i <= n; i++)
    {
        while(q.size() && q.back() < a[i]) q.pop_back();
        q.push_back(a[i]);
        if(i - k >= 1 && a[i - k] == q.front()) q.pop_front(); 
        if(i >= k) cout << q.front() << " ";

    }
}

Java

import java.io.*;
public class Main{
    static int N = 1000010;
    static int n,k;
    static int hh,tt;
    static int[] q = new int[N];
    static int[] a = new int[N];
    public static void main(String[] args)throws IOException{
        BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
        PrintWriter wt = new PrintWriter(new OutputStreamWriter(System.out));
        String[] st = bf.readLine().split(" ");
        n = Integer.parseInt(st[0]);
        k = Integer.parseInt(st[1]);
        String[] str = bf.readLine().split(" ");
        for(int i = 0 ; i < n ; i ++ ) a[i] = Integer.parseInt(str[i]);

        hh = 0;tt = - 1;
        for(int i = 0 ; i < n ; i ++ ){
            if(hh <= tt && q[hh] < i - k + 1) hh ++;
            //如果队列不是空
            //并且队列末尾元素大于新加进来的元素,就将队列末尾删掉
            //因为队列头存的是滑动窗口中最小的一个数
            while(hh <= tt && a[q[tt]] >= a[i]) tt --;  //求最小值所以保留最小值
            q[++ tt] = i;
            if(i >= k - 1) wt.print(a[q[hh]] + " ");
        }
        wt.println();
        hh = 0;tt = -1;
        for(int i = 0 ; i < n ; i ++){
            if(hh <= tt && q[hh] < i - k + 1) hh ++;
            while(hh <= tt && a[q[tt]] <= a[i]) tt --;//求最大值所以保留最大值

            q[++ tt] = i;
            if(i >= k - 1) wt.print(a[q[hh]] + " ");
        }
        wt.flush();//记得刷新流
    }
}