剑指Offer——滑动窗口的最大值

题目描述

给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。

每个窗口对应的最大值:

{[2,3,4],2,6,2,5,1}:4

{2,[3,4,2],6,2,5,1}:4

{2,3,[4,2,6],2,5,1}:6

{2,3,4,[2,6,2],5,1}:6

{2,3,4,2,[6,2,5],1}:6

{2,3,4,2,6,[2,5,1]}:5

方法一

求滑动窗口中的最大值,最直接的方法就是对于滑动窗口中的值进行一次遍历,记录最大值,将其放入返回结果vector<int> res中。

因为是滑动窗口,每个窗口相对于前一个窗口其实就是剔除了一个值,然后加入一个新的值,其余部分的值都是重复的,所以希望能够利用这个特性,减少比较操作。

很容易想到,当新加进来的值比之前的最大值temp_res要大的时候,新的滑动窗口的最大值就应该是这个新加进来的值;

但是当新加进来的值比之前的最大值temp_res要小的时候,新的滑动窗口的最大值却不一定仍然是前一个窗口的最大值,因为前一个窗口的最大值可能已经不在当前窗口中了,对于这种情况,当前一个窗口的最大值被移出时,没有想到好的办法来找到新的最大值,所以只能将当前窗口中的值进行遍历来寻找最大值。

代码如下:

class Solution {
public:
    vector<int> maxInWindows(const vector<int>& num, unsigned int size)
    {
        vector<int> res; // 返回结果
        if( size > num.size() || num.size() < 0 || size <= 0 )
            return res;
        int temp_res = num[0];
        for( int i=1;i<size;i++ ) // 寻找第一个窗口的最大值
            temp_res = max( temp_res, num[i] );
        res.push_back( temp_res );
        for( int i=1,j=size;i<=num.size()-size;i++,j++ ) // 寻找剩余窗口的最大值
        {
            if( num[j] >= temp_res ) // 新加入的值比上一个窗口的最大值大
                temp_res = num[j];
            else // 新加入的值比上一个窗口的最大值小
            {
                temp_res = num[i];
                for( int k=i;k<=j;k++ )
                    temp_res = max( temp_res, num[k] );
            }
            res.push_back( temp_res );
        }
        return res;
    }
};

方法二

上述方法依然不够好,相对于直接对每个窗口进行遍历求最大值并没有大的改进,看了剑指offer的讲解,解决了当前一个窗口的最大值被移出时,如何找到新的最大值的问题。

可以采用一个双端队列dq,将有可能成为滑动窗口最大值的数值的下标存入dq(之所以存下标,是为了方便判断数值是否已经被移出滑动窗口)。希望做到的效果是:将最大值放在dq头部,其他可能成为最大值的数放在后面。

数组的第一个数字是2,把它存入dq;第二个数字是3>2,则2不可能成为最大值了,把2删除,将3加入dq;同理,第三个数4>3,将3删除,4加入dq。此时已经是一个滑动窗口,该滑动窗口的最大值为4,放在dq的头部。

接下来为2<4,当4被移出窗口的时候,2是有可能变为最大值,则把2放入队列的尾部。

第五个数6>4且6>2,则4和2已经不可能成为后续窗口的最大值,将4和2都删除,将6加入dq。

第六个数2<6,将其加入dq尾部。

第七个数5<6,5>2,因此2已经不可能是后续窗口最大值,将2删除,5放入dq。

最后一个数1<5,将其加入dq尾部。此时要注意6已经被移出窗口,那么6应该被删除。所以此时最大值为5。判断是否需要将队列头部的值删除:因为存入的是最大值对应的下标,所以可以根据下标判断。

那么总的流程大概如下:

  • 首先处理好第一个窗口,将其最大值放入dq中;
  • 如果新来的值比队列尾部的数小,那就追加到后面,因为它可能在前面的最大值划出窗口后成为最大值;
  • 如果新来的值比尾部的大,那就删掉尾部(因为已经出现了更大的值,所以尾部数值已经不会成为最大值,所以删除),循环对新的队尾数值进行判断,直到队列为空或者队尾数值大于新来的值,再将新来的值放在队列尾部。(原本还对队列头部的值与新来的值进行了比较,但是发现前面的过程(从队尾最后一个值开始将队尾的值与新来的值进行比较,如果小于新来的值则将队尾元素删除,直到队列为空或者队尾数值大于新来的值),其实相当于已经将队列头部的值与新来的值进行了比较,所以就不需要再次比较了);
  • 如果追加的值的索引i与队列头部的值的索引只差大于等于窗口大小,即i-dq.front() >= size,那就删掉头部的值。

代码如下

class Solution {
public:
    vector<int> maxInWindows(const vector<int>& num, unsigned int size)
    {
        vector<int> res; // 返回结果
        if( size > num.size() || num.size() < 0 || size <= 0 )
            return res;
        deque<int> dq; // 双端队列存放可能成为最大值的值的下标
        for( int i=0;i<size;i++ )
        {
            if( dq.empty() )
            {
                dq.push_back(i);
                continue;
            }
            while( !dq.empty() && num[i] > num[dq.back()] ) // 循环对队尾元素与新值进行比较
                dq.pop_back();
            dq.push_back(i);
        }
        res.push_back(num[dq.front()]); // 第一个滑动窗口的最大值放入res
        for( int i=size;i<num.size();i++ )
        {
            if( i-dq.front() >= size ) // 判断队首元素是否已经移除窗口
                dq.pop_front();
            while( !dq.empty() && num[i] > num[dq.back()] ) // 循环对队尾元素与新值进行比较
                dq.pop_back();
            dq.push_back(i);
            res.push_back( num[dq.front()] ); // 将最大值放入res
        }
        return res;
    }
};

猜你喜欢

转载自blog.csdn.net/qq_36132127/article/details/81413190