棧和隊列的常見題型
一、常見題型如下:
1. 實現一個棧,要求實現Push(出棧)、Pop(入棧)、Min(返回最小值的操作)的時間復雜度為O(1)
2. 使用兩個棧實現一個隊列
3. 使用兩個隊列實現一個棧
4. 元素出棧、入棧順序的合法性。如入棧的序列(1,2,3,4,5),出棧序列為(4,5,3,2,1)
5. 一個數組實現兩個棧。
二、解法分析及示例代碼
1.第一題前兩種操作Push(出棧)、Pop(入棧)比較好實現,也是棧中比較基本的操作,題目沒有明確要求所以我們可以直接用STL容器,進行個封裝即可,stack中這兩個操作的時間復雜度就是O(1),但是第三個操作Min(返回最小值的操作)的時間復雜度為O(1),則要我們自己想辦法。思路是用兩個棧,一個就是普通的存取數據的棧,另一個為當前棧未知的最小值,因為出棧入棧都會使當前棧的最小值發生變化,所以插入數據和刪除數據兩個棧都要進行操作,返回當前棧最小值的話,就可以直接對第二個棧操作。
★使用兩個棧,s1,s2,先將第一個元素同時對s1,s2入棧,然後接著對s1入棧,將s1入棧的元素和s2.top比較,假如s2.top大於入棧元素,則把該元素對s2也入棧(?s2.top和入棧元素相等時也要入棧S2);否則將下一個元素對主棧S1入棧,依次進行;
★出棧時,把s1出棧的元素和棧頂(s2.top)比較,相等就是最小值;同時s1,s2出棧,否則s1單獨出棧。
這種方法在出現大量重復的數據情況下輔助棧也會保存大量重復的最小值數據,這時空間利用率較低。這個時候就想到了另一種方案,就是引用計數,將重復的最小值數據用引用計數來實現存儲。實現圖示如下:
代碼示例:
1 template <classT> 2 class MyStack 3 { 4 public: 5 void Push(const T& x) 6 { 7 _st.push(x); //數據棧先入棧 8 if (min_st.empty()) //如果Min棧之前沒有數據,則直接入棧 9 { 10 min_st.push(make_pair(x, 1); //x入Min棧,並置計數為1 11 } 12 else { 13 pair<T, count>& _min = min_st.top();14 if (x < _min.first) //如果入數據棧的數據小於Min棧頂數據,則也入MIn棧 15 { 16 min_st.push(make_pair(x, 1)); //x入Min棧,並置計數為1 17 }else if (x == _min.first) //如果入數據棧的數據等於Min棧頂數據,即出現冗余數據,計數加1 18 { 19 _min.second++; 20 } 21 } 22 } 23 void Pop() 24 { 25 assert(!_st.empty()); 26 pair<x, count>& _min = min_st.top(); 27 if (_st.top() == _min.first) 28 { 29 if (--_min.second == 0) //計數為1時,最小值出棧,否則計數減1 30 { 31 min_st.pop(); 32 } 33 } 34 _st.pop(); 35 } 36 T Min() 37 { 38 return min_st.top(); 39 } 40 private: 41 stack<T> _st; //數據棧 42 stack<pair<T, count>> min_st; //最小值棧 43 };
2.第二題思路是:兩個棧s1,s2,始終維護s1作為存儲空間,以s2作為臨時緩沖區。
入隊時,將元素壓入s1。
出隊時,將s1的元素逐個“倒入”(彈出並壓入)s2,將s2的頂元素彈出作為出隊元素,之後再將s2剩下的元素逐個“倒回”s1。
見下面示意圖:
實現代碼如下:
1 template <class T> 2 class TwoStackQueue 3 { 4 public: 5 void Push(T k) 6 { 7 s1.push(k); 8 } 9 void Pop() 10 { 11 if (s1.empty() && s2.empty() == false) { 12 while (s1.empty()) 13 { 14 s2.push(s1.top()); 15 s1.pop(); 16 } 17 s2.pop(); 18 while (s2.empty()) 19 { 20 s1.push(s2.top()); 21 s2.pop(); 22 } 23 } 24 } 25 private: 26 stack<T> s1; 27 stack<T> s2; 28 };
這個題還可以優化一下。即:在出隊時,將s1的元素逐個“倒入”s2時,原在s1棧底的元素,不用“倒入”s2(即只“倒”s1.Count()-1個),可直接彈出作為出隊元素返回。這樣可以減少一次壓棧的操作。
上述思路,有些變種,如:
入隊時,先判斷s1是否為空,如不為空,說明所有元素都在s1,此時將入隊元素直接壓入s1;如為空,要將s2的元素逐個“倒回”s1,再壓入入隊元素。
出隊時,先判斷s2是否為空,如不為空,直接彈出s2的頂元素並出隊;如為空,將s1的元素逐個“倒入”s2,把最後一個元素彈出並出隊。
相對於第一種方法,變種的s2好像比較“懶”,每次出隊後,並不將元素“倒回”s1,如果趕上下次還是出隊操作,效率會高一些,但下次如果是入隊操作,效率不如第一種方法。我有時會讓面試者分析比較不同方法的性能。我感覺(沒做深入研究),入隊、出隊操作隨機分布時,上述兩種方法總體上時間復雜度和空間復雜度應該相差無幾(無非多個少個判斷)。
真正性能較高的,其實是另一個變種。即:
入隊時,將元素壓入s1。
出隊時,判斷s2是否為空,如不為空,則直接彈出頂元素;如為空,則將s1的元素逐個“倒入”s2,把最後一個元素彈出並出隊。
這個思路,避免了反復“倒”棧,僅在需要時才“倒”一次。
3.第三題思路是:q1是專職進出棧的,q2只是個中轉站
入棧:直接入隊列q1即可
出棧:把q1的除最後一個元素外全部轉移到隊q2中,然後把剛才剩下q1中的那個元素出隊列。之後把q2中的全部元素轉移回q1中。
代碼示例:
1 template<class T> 2 class Queue 3 { 4 public: 5 void Push(const T& x) 6 { 7 _in.push(x); 8 } 9 10 void Pop() 11 { 12 assert(!_in.empty() || !_out.empty()); 13 14 if (_out.empty()) 15 { 16 while (!_in.empty()) 17 { 18 _out.push(_in.top()); 19 _in.pop(); 20 } 21 } 22 23 _out.pop(); 24 } 25 26 T& Front() 27 { 28 assert(!_in.empty() || !_out.empty()); 29 30 if (_out.empty()) 31 { 32 while (!_in.empty()) 33 { 34 _out.push(_in.top()); 35 _in.pop(); 36 } 37 } 38 39 return _out.top(); 40 } 41 42 43 private: 44 stack<T> _in; 45 stack<T> _out; 46 };
思路二:
q1是專職進出棧的,q2只是個中轉站。元素集中存放在一個棧中,但不是指定(q1 或 q2)。定義兩個指針:pushtmp:指向專門進棧的隊列q1; tmp:指向臨時作為中轉站的另一個棧q2
入棧:直接入pushtmp所指隊列即可
出棧:把pushtmp的除最後一個元素外全部轉移到隊列tmp中,然後把剛才剩下q1中的那個元素出隊列
示例代碼二:
1 void Push(Queue *q1, Queue *q2, int k) 2 { 3 Queue *pushtmp; 4 if(!IsQueueEmpty(q1)) 5 { 6 pushtmp = q1; 7 } 8 else 9 { 10 pushtmp = q2; 11 } 12 EnQueue(pushtmp, k); 13 } 14 15 int Pop(Queue *q1, Queue *q2) 16 { 17 int tmpvalue; 18 Queue *pushtmp, *tmp; 19 if(!IsQueueEmpty(q1)) 20 { 21 pushtmp = q1; 22 tmp = q2; 23 } 24 else 25 { 26 pushtmp = q2; 27 tmp = q1; 28 } 29 30 if(IsQueueEmpty(pushtmp)) 31 { 32 printf("Stack Empty!\n"); 33 } 34 else 35 { 36 while(SizeOfQueue(pushtmp) != 1) 37 { 38 EnQueue(tmp, DeQueue(pushtmp)); 39 } 40 tmpvalue = DeQueue(pushtmp); 41 return tmpvalue; 42 } 43 }
比較: 在第二種思路中,出棧後就不用轉移回原來的棧了(圖示最後一步),這樣減少了轉移的次數。
4.第四題是比較常見的判斷出棧順序是否合法性的題目,思路是:
1)用一個 st 棧做入棧序列(1,2,3,4,5)出入棧操作,然後定義指針 in 和 out ,in 指向入棧序列,out 指向需要判斷的出棧序列;
2)將入棧序列第一個數據入棧,如果此數據與第一個出棧序列數據相等,則再出棧,再比較兩個序列的第二個數據;
3)若不相等,入棧序列第二個數據入棧,再與出棧序列比較;
4)若入棧序列遍歷完且臨時棧已空,而出棧序列沒完,則不合法,若出棧序列也遍歷完,則合法。
示例代碼:
1 bool IsInvalidStack(char* in, char* out) 2 { 3 assert(in && out); 4 5 stack<char> st; 6 while (1) 7 { 8 while (!st.empty()) 9 { 10 if (*out && st.top() == *out) 11 { 12 st.pop(); 13 ++out; 14 } 15 else 16 { 17 break; 18 } 19 } 20 21 if (*in) 22 { 23 st.push(*in); 24 ++in; 25 } 26 else 27 { 28 if (*out) 29 { 30 return false; 31 } 32 else 33 { 34 return true; 35 } 36 } 37 } 38 } 39 void TestIsInvalidStack() 40 { 41 char* in = "12345"; 42 char* out = "45321"; 43 cout<<IsInvalidStack(in, out)<<endl; 44 }
5.第5題有多種思路解題
①奇偶位存儲:數組的奇數位置存儲一個棧的元素,偶數位置存儲另一個棧的元素;
示例代碼:
1 template<class T, size_t N> 2 class DoubleStack 3 { 4 public: 5 DoubleStack() 6 :_top1(-2) 7 ,_top2(-1) 8 {} 9 10 void Check(int pos) 11 { 12 if (pos >= _a.size()) 13 { 14 _a.resize(pos*2); 15 } 16 } 17 18 void Push1(const T& x) 19 { 20 Check(_top1+2); 21 _a[_top1+2] = x; 22 } 23 24 void Push2(const T& x) 25 { 26 _a[_top2+2] = x; 27 } 28 29 private: 30 vector<T> _a; 31 int _top1; 32 int _top2; 33 };
②左右存儲:兩個棧分別從數組的中間向兩頭增長; 數組的中間位置看做兩個棧的棧底,壓棧時棧頂指針分別向兩邊移動,當任何一邊到達數組的起始位置或是數組尾部,則開始擴容;
實際上方法二與方法一空間利用率相當本質上相同,所以方法二代碼就不實現了。
③兩端生長:兩個棧分別從數組的兩頭開始增長。 將數組的起始位置看作是第一個棧的棧底,將數組的尾部看作第二個棧的棧底, 壓棧時,棧頂指針分別向中間移動,直到兩棧頂指針相遇,則擴容;
示例代碼:
1 //兩個棧分別從數組的兩頭開始向中間增長。 2 template <class T> 3 class DoubleStack2 4 { 5 public: 6 void Push(int index, T data) 7 { 8 if (top1 > top2) //已滿 9 return; 10 if (index == 0) { //對棧1操作 11 top1++; 12 array[top1] = data; 13 } 14 else if(index == 1) { //對棧2操作 15 top2--; 16 array[top2] = data; 17 } 18 } 19 void Pop(int index) 20 { 21 if (index == 0) { //對棧1操作 22 if (top1 >= 0) //棧1不為空時 23 top1--; 24 } 25 else if (index == 1) { //對棧2操作 26 if (top2 < MAX) //棧2不為空時 27 top2++; 28 } 29 } 30 T& top(int index) 31 { 32 if (index == 0 && top1 > 0) { //對棧1操作 33 return array[top1 - 1]; 34 } 35 if (index ==1 && top2 < MAX) { 36 return array[top2 + 1] 37 } 38 } 39 private: 40 T* array[MAX]; 41 int top1; 42 int top2; 43 };
前兩種方法在一定的情況下會造成空間的浪費,比如當一個棧存滿了而另一個棧還是空的,這時候空間利用率不高,所以建議采用第三種方式完成。當一個棧一直入棧,另一個棧一直出棧時,方法三的空間利用率是比較好的。
棧和隊列的常見題型