1. 程式人生 > >劍指Offer--065-滑動視窗的最大值

劍指Offer--065-滑動視窗的最大值

1 連結

| 牛客OJ | 九度OJ | CSDN題解 | GitHub程式碼 |
| ——- |:——-:|:——-:| ——-:|
065-滑動視窗的最大值

2 題意

題目描述

題目描述

給定一個數組和滑動視窗的大小,找出所有滑動窗口裡數值的最大值

例如,如果輸入陣列{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},最大值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

3 暴力解法

如果採用蠻力法,這個問題似乎不難解決:可以掃描每一個滑動視窗的所有數字並找出其中的最大值。如果滑動視窗的大小為k,需要O(k)時間才能找出滑動窗口裡的最大值。對於長度為n的輸入陣列,這個演算法總的時間複雜度是O(nk)

#include <iostream>
#include <vector>
#include <deque>
#include <iterator> #include <climits> using namespace std; // 除錯開關 #define __tmain main #ifdef __tmain #define debug cout #else #define debug 0 && cout #endif // __tmain /* 單調佇列,O(n) * 引用馬客(Mark)的解題思路,馬客沒加註釋,我用自己的理解加下注釋,希望對你們有用, * 如有錯誤,見諒,我會及時修改 * deque s中儲存的是num的下標 */
class Solution { public: vector<int> maxInWindows(const vector<int>& num, unsigned int size) { vector<int> res; if(num.size() == 0 || size == 0) { return res; } for(int start = 0; start <= (int)(num.size( ) - size); start++) { int end = start + size; int max = INT_MIN; for(int index = start; index < end; index++) { if(num[index] > max) { max = num[index]; } } debug <<"[" <<start <<", " <<end <<"], max = " <<max <<endl; res.push_back(max); } return res; } }; int __tmain( ) { Solution solu; int array[] = { 2, 3, 4, 2, 6, 2, 5, 1 }; vector<int> vec(array, array + 8); vector<int> res = solu.maxInWindows(vec, 3); copy(res.begin( ), res.end( ), ostream_iterator<int>(cout," ")); return 0; }

4 佇列中取最大值操作問題

實際上一個滑動視窗可以看成是一個佇列。當視窗滑動時,處於視窗的第一個數字被刪除,同時在視窗的末尾新增一個新的數字。這符合佇列的先進先出特性。如果能從佇列中找出它的最大數,這個問題也就解決了。

在面試題21中。我們實現了一個可以用O(1)時間得到最小值的棧。同樣,也可以用O(1)時間得到棧的最大值。同時在面試題7中,我們討論瞭如何用兩個棧實現一個佇列。綜合這兩個問題的解決方法,我們發現如果把佇列用兩個棧實現,由於可以用O(1)時間得到棧中的最大值,那麼也就可以用O(1)時間得到佇列的最大值,因此總的時間複雜度也就降到了O(n)

因此我們現在的問題歸結為, 實現一個儘可能快的找出佇列最大值

起初沒仔細看,還以為與此前的自定義棧-pop-push-min-時間複雜度都為O(1) 是一樣的,後來才發現不是一回事,有差別的。對於棧來說,我們可以的入棧和出棧不會影響輔助陣列內的情況,假設當前N個元素,(為了說明簡單,下標從1開始),輔助空間的F[1]記錄的是A[1,1]內的最值位置,F[2]記錄的是A[1,2]內的最值位置,···,F[N]記錄的是A[1,N]內的最值位置。在插入F[k+1]=A[F[k]]與A[k+1]兩者最值的位置,插入複雜度為O(1),在刪除第k+1個節點時,刪除A[k+1]和F[k+1],這並不影響F[1]-F[k],因此刪除的複雜度為O(1),取最值,假設當前N個元素,即返回A[F[N]]。
對於佇列來說,如果套用棧的輔助陣列方法,假設當前有N個元素,下標從1開始,那麼F[1]記錄的是A[1,N]中的最值,F[2]記錄的是A[2,N]中的最值,···,F[N]記錄的是A[N,N]中的最值。當A[k+1]入隊時,需要更新F[1]到F[k],F[K+1]=K+1,因此插入的複雜度為O(N),當F[k]出隊時,刪除F[k]即可(每次出隊的都是第一個元素,實際上此時F[1]-F[k-1]已經出隊完畢了),因此刪除的複雜度為O(1)。取最值的複雜度是O(1)。

佇列與棧的區別很清楚了。我們在程式設計之美上找到了兩個答案
* 一個是構建最大堆
* 另一個是用兩個棧來實現。

4.1 最大堆的方法

佇列本身要麼順序結構要麼連結結構,還那麼存。另外對於佇列每個元素構建一個節點(包含在佇列中的位置),這些節點構成一個最大堆,因此插入和刪除操作都要維護這個最大堆,時間複雜度都是O(LogN),取最大值的複雜度為 O(1)。

暴力的思路簡單,但是時間複雜度過高,因此需要改進。可以使用一個最大堆來儲存size個數字,每次插入數字時只需要O(lgsize)的時間,從堆中取最大值只需要O(1)的時間。

隨著視窗由左向右滑動,因此堆中有些數字會失效(因為它們不再包含在視窗中)。

class Solution
{
    typedef pair<int,int> Pair;
public :
    vector<int> maxInWindows(const vector<int> &num, unsigned int size)
    {
        vector<int> result;
        priority_queue<Pair> Q;
        if (num.size() < size || size < 1)
        {
            return result;
        }

        for (int i = 0; i < size-1; i++)
        {
            Q.push(Pair(num[i],i));
        }

        for (int i = size-1; i < num.size(); i++)
        {
            Q.push(Pair(num[i],i));
            Pair p = Q.top();
            while(p.second < i-(size-1)) {
                Q.pop();
                p = Q.top();
            }
            result.push_back(p.first);
        }
//        result.push_back(Q.top().first);
        return result;
    }
};

4.2 兩個棧的方法

A棧,B棧,這兩個棧都是前面提到的pop-push-min複雜度都為O(1)的空間換時間的實現。
取最值:返回A棧的最值和B棧的最值相比後的最值。複雜度O(1)。

入隊操作:直接入到B棧中。複雜度O(1)。
出隊操作:如果A棧不為空,直接A棧出棧,複雜度為O(1),如果A棧為空,那麼將B棧內容逐個出棧並且逐個入棧到A中,然後A棧出棧,複雜度O(N),實際上是B棧的長度。

對於這種方法,如果對列的操作時,一連串的入棧,然後是一連串的出棧,那麼就是首先不停向B入棧,然後第一個出棧,B棧元素全壓入A棧,A出棧一個,這一步是N的複雜度,但是此後是不停的從A出棧,這都是O (1)的複雜度。還不錯呢。而且藉助了棧的程式碼,方便實現。對於這樣的情景,就是隻有第一個出棧的時候,要O(N),複雜度不是很均勻。對於每個元素來說,要麼入B棧,入A棧,從A棧彈出,即總體是3N,平均下來基本上是O(3),要不最大堆的O(LogN)是快了不少呢。

#include <iostream>
#include <vector>
#include <iterator>

#include <climits>

using namespace std;


//  除錯開關
#define __tmain main

#ifdef __tmain

#define debug cout

#else

#define debug 0 && cout

#endif // __tmain


#define MAX 100

class Stack
{
private:
    int stackItem[MAX];
    int link2NextMaxValueIndex[MAX];
    int stackTop;
    int maxValueIndex;
public:
    Stack() : stackTop(-1), maxValueIndex(-1) {}
    int size() { return stackTop + 1; }
    int empty() { return stackTop < 0 ? 1 : 0; }

    void push(int val)
    {
        ++stackTop;
        if(stackTop == MAX)
        {
            cout << "The stack has been full!" << endl;
            return;
        }
        else
        {
            stackItem[stackTop] = val;
            if(max() < val)
            {
                link2NextMaxValueIndex[stackTop] = maxValueIndex;
                maxValueIndex = stackTop;
            }
            else
                link2NextMaxValueIndex[stackTop] = -1;
        }
    }

    int pop()
    {
        int ret;
        if(stackTop == -1)
        {
            cout << "The stack is empty!" << endl;
            return -1;
        }
        else
        {
            ret = stackItem[stackTop];
            if(stackTop == maxValueIndex)
            {
                maxValueIndex = link2NextMaxValueIndex[stackTop];
            }
            --stackTop;

            return ret;
        }
    }

    int max()
    {
        if(maxValueIndex >= 0)
            return stackItem[maxValueIndex];
        else
            return -100;
    }

};
class Queue
{
private:
    Stack stackIn;
    Stack stackOut;
public:
    int size( )
    {
        return stackIn.size( ) + stackOut.size( );
    }
    int max( )
    {
        return std::max(stackIn.max( ), stackOut.max( ));
    }

    void enQueue(int val)
    {
        stackIn.push(val);
    }

    int deQueue()
    {
        if(stackOut.empty() and !stackIn.empty())
        {
            while(!stackIn.empty())
                stackOut.push(stackIn.pop());
        }
        return stackOut.pop();
    }
};

class Solution
{
public      :
   vector<int> maxInWindows(const vector<int>& num, unsigned int size)
   {
        unsigned int    length = num.size( );
        vector<int>     res;

        if(length == 0 || size == 0 || length < size)
        {
            return res;
        }

        Queue           que;
        for(int i = 0; i < num.size( ); i++)
        {
            if(que.size( ) < size)
            {
                que.enQueue(num[i]);
            }
            else
            {
                res.push_back(que.max( ));

                que.enQueue(num[i]);
                que.deQueue( );
            }
        }
        if(que.size( ) == size)
        {
            res.push_back(que.max( ));
        }

        return res;
    }


};

int __tmain()
{
    Solution solu;

    int array[] = { 2, 3, 4, 2, 6, 2, 5, 1 };
    vector<int> vec(array, array + 8);

    vector<int> res = solu.maxInWindows(vec, 3);
    copy(res.begin( ), res.end( ), ostream_iterator<int>(cout," "));

    return 0;
}

5 單調佇列

還是把滑動視窗當成是佇列來處理,其實是最大值佇列的改進策略, 思路基本類似, 但是佇列中儲存的是最大值的下標, 為了得到滑動視窗的最大值,佇列序可以從兩端刪除元素,因此使用雙端隊


原則:

對新來的元素k,將其與雙端佇列中的元素相比較

  • 前面比k小的,直接移出佇列(因為不再可能成為後面滑動視窗的最大值了!),

  • 前面比k大的X,比較兩者下標,判斷X是否已不在視窗之內,不在了,直接移出佇列

佇列的第一個元素是滑動視窗中的最大值

class Solution
{
public :
    /*  方式二:利用佇列來解決,時間複雜度為O(n)
        利用雙端佇列來實現單調佇列(索引對應的值是單調的)  */
   vector<int> maxInWindows(const vector<int>& num, unsigned int size)
   {
        unsigned int length = num.size( );
        vector<int> result;

        if(length == 0 || size == 0 || length < size)
        {
            return result;
        }

        deque<int> indexQueue;

        /*  第一個視窗的處理比較簡單, 直接找到最大的那個即可  */
        for(unsigned int i = 0;
            i < size;
            i++)
        {
            /*  刪除隊尾元素
             *  對於當前元素num[i]
             *  前面比k小的,直接移出佇列
             *  因為不再可能成為後面滑動視窗的最大值了  */
            while(indexQueue.empty( ) != true
               && num[i] >= num[indexQueue.back( )])
            {
                indexQueue.pop_back( );
            }
            /*  將當前元素的下表壓入佇列中  */
            indexQueue.push_back(i);
        }

        /* 處理後續的滑動視窗*/
        for(unsigned int i = size;
            i < length;
            i++)
        {
            /*  佇列中的第一個元素是當前滑動視窗最大值的下標  */
            result.push_back(num[indexQueue.front()]);

            /*  刪除隊尾元素
             *  對於當前元素num[i]
             *  前面比k小的,直接移出佇列
             *  因為不再可能成為後面滑動視窗的最大值了  */
            while(indexQueue.empty( ) != true
               && num[i]>=num[indexQueue.back()])
            {
                indexQueue.pop_back();
            }

            /*  刪除隊首元素
             *  前面比k大的X,
             *  比較兩者下標,判斷X是否已不在視窗之內,
             *  不在了,直接移出佇列  */
            if(indexQueue.empty( ) != true
            && indexQueue.front( ) < (int)(i - size + 1))
            {
                indexQueue.pop_front( );
            }
            indexQueue.push_back(i);
        }
        result.push_back(num[indexQueue.front()]);
        return result;
    }
};

下面程式碼是網上看到的, 其實一樣的思路, 只是寫法更簡練

/*  單調佇列,O(n)
 *  deque s中儲存的是num的下標
 *
 *  題目:滑動視窗的最大值
 *
 *  思路:滑動視窗應當是佇列,但為了得到滑動視窗的最大值,佇列序可以從兩端刪除元素,因此使用雙端佇列。
 *
 *  原則:
 *  對新來的元素k,將其與雙端佇列中的元素相比較
 *  1.  前面比k小的,直接移出佇列(因為不再可能成為後面滑動視窗的最大值了!),
 *  2.  前面比k大的X,比較兩者下標,判斷X是否已不在視窗之內,不在了,直接移出佇列
 *  佇列的第一個元素是滑動視窗中的最大值
 *  */

class Solution
{
public:
    vector<int> maxInWindows(const vector<int>& num, unsigned int size)
    {
        vector<int>     res;
        deque<int>      index;
        for(unsigned int i = 0; i < num.size( ); i++)
        {
            cout <<"size["<<index.size( ) <<"] : ";
            copy(index.begin( ), index.end( ), ostream_iterator<int>(cout," "));
            cout <<endl;

            /*  從後面依次彈出佇列中比當前num值小的元素,
             *  同時也能保證佇列首元素為當前視窗最大值下標  */
            while(index.size( ) != 0 && num[index.back( )] <= num[i])
            {
                index.pop_back( );
            }
            /*  當前視窗移出隊首元素所在的位置
                即隊首元素座標對應的num不在視窗中,需要彈出  */
            while(index.size() && i - index.front( ) + 1 > size)
            {
                index.pop_front( );
            }

            /*  把每次滑動的num下標加入佇列  */
            index.push_back(i);

            /*  當滑動視窗首地址i大於等於size時才開始寫入視窗最大值  */
            if(size != 0 && i + 1 >= size)
            {
                res.push_back(num[index.front( )]);
            }
        }
        return res;
    }
};