【佇列】滑動視窗的最大值序列,帶max函式的佇列
阿新 • • 發佈:2018-12-17
視窗即佇列,本質是一樣的。
面試題59-1:滑動視窗的最大值序列
給定一個數組和滑動視窗的大小,請找出所有滑動窗口裡的最大值。例如,如果輸入陣列{2, 3, 4, 2, 6, 2, 5, 1}及滑動視窗的大小3,那麼一共存在6個滑動視窗,它們的最大值分別為{4, 4, 6, 6, 6, 5}。
用雙端佇列儲存最大值的下標,雙端佇列在視窗滑動過程中變化更新,其隊頭元素表示當前視窗中最大值的下標。
其實可以想象成一個年輕人和老年人比較的模型,年輕人就是陣列中靠後的數字,老年人就是靠前的數字,滑動視窗右邊滑入就相當於年輕人出生,滑動視窗的大小就是人的壽命,滑動視窗左邊滑出就是老年人死亡。而需要的雙端佇列正是記錄視窗中“最優秀的人”,用數字表示優秀的程度,年輕人比老年人數字高,那麼這個老年人在整個過程中都要受制於年輕人,因為這個年輕人已經出生了,他在視窗中的停留時間會比老年人的剩餘壽命長,這時就要把這樣的老年人移除雙端佇列。
而如果出生了不那麼優秀的年輕人,一樣要保留下來,因為它的剩餘壽命更長,待老年人從滑動視窗滑出後還是可能成為最優秀的(數字最大的)。但同樣,接下來如果出生了更優秀的人(更年輕),就要把剛剛的年輕人(相比這個他就是老年人)移除了。
#include<bits/stdc++.h>
using namespace std;
//輸入陣列和視窗大小,輸出滑動視窗的最大值陣列
vector<int> maxInWindows(const vector<int>& num, unsigned int size) {
vector<int> maxInWindows; //滑動視窗的最大值陣列
if(num.size() >= size && size >= 1) {//陣列要比視窗大,視窗要不少於1
deque<int> index;//雙端佇列,用於記錄視窗中可能的最大值所在陣列中下標
//視窗初始狀態下會覆蓋陣列的前size個位置
for(unsigned int i = 0; i < size; ++i) {
//如果新的這個數字比視窗中的一些數字大
//注意是比尾部的一些數字大
//比如[4213]...,先進4;
//遇到2比尾部4小但在滑動中可能成為最大,尾插2
//遇到1比尾部2小但在滑動中可能成為最大,尾插1
//遇到3比尾部1大,因為只要1還在窗口裡3一定在
//所以一定輪不到1做最大,把1尾刪
//同理,接著遇到2,把2尾刪
//接著遇到4,3比4小,4劃出去了3可能還是最大
while(!index.empty() && num[i] >= num[index.back()])//等於的時候也刪不影響,相當於用大的遊標了
index.pop_back();
//尾部插入是一定會插入的,就像前面的分析過程
//因為不可能根據前面的老數字判斷年輕的自己沒有成為最大的可能
//只可能在下次for迴圈中看到更年輕且更大的數字,才能斷定自己沒有可能做最大了
index.push_back(i);
}
//前面初始化判斷過了0~size-1位置,接下來是滑動過程
//即從size位置到最後的num.size()-1陣列尾
for(unsigned int i = size; i < num.size(); ++i) {
//在每次滑動之前寫入當前視窗的最大值
//即下標為index隊頭的陣列元素
maxInWindows.push_back(num[index.front()]);
//視窗向前滑動(沒有任何程式碼),表現在接下來要使用i的值了
//或者可以說for裡的++i就是視窗向前滑動1
//但這樣也不好理解上一句是"滑動之前寫入"
//滑動後,用同樣的邏輯調整存最大值下標的雙端佇列
while(!index.empty() && num[i] >= num[index.back()])
index.pop_back();
//如果當前雙端佇列的最大值下標不超過當前下標減去滑動視窗大小
if(!index.empty() && index.front() <= (int) (i - size))
index.pop_front();//就說明這個最大值剛好滑出來了,將其刪除
//和前面類似地,將當前位置的下標壓入
//只有後續的年輕數字才有淘汰自己的資格
index.push_back(i);
}
//最後一次滑動還沒寫入,將其最大值寫入
maxInWindows.push_back(num[index.front()]);
}
return maxInWindows;//返回滑動視窗的最大值陣列
}
int main() {
int num[] = { 2, 3, 4, 2, 6, 2, 5, 1 };
vector<int> vPut;
for(unsigned int i=0;i<sizeof(num)/sizeof(int);i++){
vPut.push_back(num[i]);
}
vector<int> vGet=maxInWindows(vPut,3);
for(vector<int>::const_iterator cit=vGet.cbegin();cit!=vGet.cend();cit++){
cout<<*cit<<"\t";//4 4 6 6 6 5
}
return 0;
}
面試題59-2:帶max函式的佇列
注意,這題在作者的GitHub上的題目描述寫錯了,寫成了59-1即上面那題的描述了。
請定義一個佇列並實現函式max得到佇列裡的最大值,要求函式max、push_back和pop_front的時間複雜度都是O(1)。
在入隊時賦予一個下標值(就像註冊使用者給個自增的id),這樣就可以用和59-1一樣的思路去解決了。入隊就相當於有元素滑進視窗,出隊就相當於有元素滑出視窗。前者需要判斷要刪除的老元素,後者需要判斷當前窗口裡的最大值有沒有離開視窗。
#include<bits/stdc++.h>
using namespace std;
//帶max函式的佇列模板類
template<typename T> class QueueWithMax {
private:
//這個自定義佇列的元素結點結構體
struct InternalData {
T number;//元素的值,泛型型別
int index;//插入時被賦予的下標
};
deque<InternalData> data;//存佇列實際內容的雙端佇列
deque<InternalData> maximums;//存最大值的雙端佇列
int currentIndex;//接下來要插入元素應該賦予的下標
public:
//構造器
QueueWithMax() : currentIndex(0) {//初始化下標值為0
}
//入隊,相當於上一題裡面滑動視窗滑入一個數字
void push_back(T number) {
//和老數字比較,把雙端佇列裡不比他大的老元素都刪掉
while(!maximums.empty() && number >= maximums.back().number)
maximums.pop_back();
InternalData internalData = { number, currentIndex };//建立元素結點
data.push_back(internalData);//進入存元素的佇列
maximums.push_back(internalData);//也進入最大值佇列
++currentIndex;//id自增1,用來給下一個插入的元素用
}
//出隊,相當於滑出視窗,不過這裡可以視為視窗從左側內陷1造成的
void pop_front() {
if(maximums.empty())//佇列為空不能出隊
throw new exception();//queue is empty
//這個就比上一題的滑出容易多了,直接判斷是不是當前最大
if(maximums.front().index == data.front().index)
maximums.pop_front();//如果是,就把那個當前最大也拿掉(從隊頭)
data.pop_front();//出隊
}
//獲取最大值
//尾接const表示成員函式隱含傳入的this指標為const指標
//即在這個成員函式內不能修改非mutable成員
T max() const {
if(maximums.empty())//為空就沒有最大值
throw new exception();//queue is empty
//存最大值的雙端佇列的隊頭元素的number域
return maximums.front().number;
}
};
int main() {
QueueWithMax<int> queue;
queue.push_back(2);
cout<<queue.max()<<endl;//2
queue.push_back(4);
cout<<queue.max()<<endl;//4
queue.push_back(3);
cout<<queue.max()<<endl;//4
queue.push_back(2);
cout<<queue.max()<<endl;//4
queue.pop_front();
cout<<queue.max()<<endl;//4
queue.pop_front();
cout<<queue.max()<<endl;//3
return 0;
}