今天我們來學習下棧。那麼什麼是棧呢?棧是一種特殊的線性表,它僅能在線性表的一段進行操作。棧頂(Top)是允許操作的一端,棧底(Bottom)是不允許操作的一端。棧的特性是後進先出(last in first out),另外一種說法就是先進後出。如下圖所示
下來我們來看看棧的操作,有如下操作:
1、創建棧(Stack())
2、銷燬棧(~Stack())
3、清空棧(clear())
4、進棧(push())
5、出棧(pop())
6、獲取棧頂元素(top())
7、獲取棧的大小(size())
下來我們來看看棧的實現,處於如下位置。因爲它是一個抽象父類,因此其成員函數都作爲純虛函數。
棧的順序實現如下圖所示
下來來看看 StaticStack 的設計要點:首先必須是一個類模板,使用原生數組作爲棧的存儲空間,使用模板參數決定棧的最大容量。下來看看 StaticStack 是怎樣實現的,源碼如下
Stack.h 源碼
#ifndef STACK_H #define STACK_H #include "Object.h" namespace DTLib { template < typename T > class Stack : public Object { public: virtual void push(const T& e) = 0; virtual void pop() = 0; virtual T top() const = 0; virtual void clear() = 0; virtual int size() const = 0; }; } #endif // STACK_H
StaticStack.h 源碼
#ifndef STATICSTACK_H #define STATICSTACK_H #include "Stack.h" #include "Exception.h" namespace DTLib { template < typename T, int N > class StaticStack : public Stack<T> { protected: T m_space[N]; int m_top; int m_size; public: StaticStack() { m_top = -1; m_size = 0; } int capacity() const { return N; } void push(const T& e) { if( m_size < N ) { m_space[m_top + 1] = e; m_top++; m_size++; } else { THROW_EXCEPTION(INvalidOPerationException, "No space in current stack ..."); } } void pop() { if( m_size > 0 ) { m_top--; m_size--; } else { THROW_EXCEPTION(INvalidOPerationException, "No element in current stack ..."); } } T top() const { if( m_size > 0) { return m_space[m_top]; } else { THROW_EXCEPTION(INvalidOPerationException, "No element in current stack ..."); } } void clear() { m_top = -1; m_size = 0; } int size() const { return m_size; } }; } #endif // STATICSTACK_H
我們來測試下這個代碼,看看那有沒有什麼問題
#include <iostream> #include "StaticStack.h" using namespace std; using namespace DTLib; int main() { StaticStack<int, 5> stack; try { stack.pop(); } catch(const Exception& e) { cout << e.message() << endl; cout << e.location() << endl; } for(int i=0; i<5; i++) { stack.push(i); } while( stack.size() > 0 ) { cout << stack.top() << endl; stack.pop(); } return 0; }
編譯結果如下
我們看到在剛開始棧中沒有數據元素的情況下,就直接 pop 肯定會出錯,所以 try...catch 捕獲到了異常信息,下面的輸出跟我們想的是一致的。所以這個代碼是沒錯的。那麼此時我們已經完成了 StaticStack 類的代碼,但是我們有沒有考慮下這種情況:那便是當存儲的元素爲類類型時,StaticStack 的對象在創建時,會多次調用元素類型的構造函數,便會極大的影響效率。下來我們來試試,測試代碼如下
#include <iostream> #include "StaticStack.h" using namespace std; using namespace DTLib; class Test : public Object { public: Test() { cout << "Test()" << endl; } ~Test() { cout << "~Test()" << endl; } }; int main() { StaticStack<Test, 5> stack; cout << stack.size() << endl; return 0; }
我們創建了 5 個 StaticStack 元素的對象,它是原生數組泛指類型的。但是我們並沒有進行 push 操作,因此它的大小還是爲 0。我們來看看打印信息
我們看到確實是觸發了構造和析構函數,這便極大的影響了效率。那麼我們此時是不是需要一種新的棧的存儲方式呢?我們可以開發出一種新的鏈式棧,它的實現方式如下圖所示
那麼鏈式棧的設計要點如下:
1、類模板,抽象父類 Stack 的直接子類;
2、在內部組合使用 LinkList 類,實現棧的鏈式存儲;
3、只在單鏈表成員對象的頭部進行操作。
我們來看看鏈式棧的結構,如下
鏈式棧的源碼如下
LinkStack.h 源碼
#ifndef LINKSTACK_H #define LINKSTACK_H #include "Stack.h" #include "LinkList.h" #include "Exception.h" namespace DTLib { template < typename T > class LinkStack : Stack<T> { protected: LinkList<T> m_list; public: void push(const T& e) { m_list.insert(0, e); } void pop() { if( m_list.length() > 0 ) { m_list.remove(0); } else { THROW_EXCEPTION(INvalidOPerationException, "No element in current stack ..."); } } T top() const { if( m_list.length() > 0 ) { return m_list.get(0); } else { THROW_EXCEPTION(INvalidOPerationException, "No element in current stack ..."); } } void clear() { m_list.clear(); } int size() const { return m_list.length(); } }; } #endif // LINKSTACK_H
我們再用 LinkStack 類重試剛纔的實驗,看看還沒有效率的問題,編譯運行如下
我們看到此時顯然已經不再調用 Test 類的構造函數了。下來我們來看看一個棧的應用:符號匹配問題。在 C 語言中有一些成對出現的符號,如括號:(), [], {}, <>,再如引號:' ', " "。該如何實現編譯器中的符號成對檢測呢?
算法思路:
1、從第一個字符開始掃描:當遇見普通字符時忽略;當遇見左符號時壓入棧中;當遇見右符號時彈出棧頂符號,並進行匹配。
2、結束:成功時,所有字符掃描完畢,且棧爲空;失敗時,匹配失敗或所有字符掃描完畢但棧非空。
編寫代碼如下
#include <iostream> #include "LinkStack.h" using namespace std; using namespace DTLib; bool is_left(char c) { return (c == '(') || (c == '[') || (c == '{'); } bool is_right(char c) { return (c == ')') || (c == ']') || (c == '}'); } bool is_quot(char c) { return (c == '\'') || (c == '\"'); } bool is_match(char l, char r) { return ( (l == '(') && (r == ')') ) || ( (l == '{') && (r == '}') ) || ( (l == '[') && (r == ']') ) || ( (l == '<') && (r == '>') ) || ( (l == '\'') && (r == '\'') ) || ( (l == '\"') && (r == '\"') ); } bool scan(const char* code) { LinkStack<char> stack; int i=0; bool ret = true; code = (code == NULL) ? "" : code; while( ret && (code[i] != '\0') ) { if( is_left(code[i]) ) { stack.push(code[i]); } else if( is_right(code[i]) ) { if( (stack.size() > 0) && is_match(stack.top(), code[i]) ) { stack.pop(); } else { ret = false; } } else if( is_quot(code[i]) ) { if( (stack.size() == 0) || !is_match(stack.top(), code[i]) ) { stack.push(code[i]); } else if( is_match(stack.top(), code[i]) ) { stack.pop(); } } i++; } return ret && (stack.size() == 0); } int main() { cout << scan("abcd") << endl; return 0; }
我們看看編譯運行的結果是否爲 1。
我們再將上面的 scan 中的內容改爲 \"<a{b(\'e\')c}d>\" 看看結果是否還是匹配完成
我們將前面的雙引號去掉看看結果
我們看到已經實現了符號的成對檢測功能。通過今天對棧的學習,總結如下:1、棧是一種特殊的線性表,棧值允許在線性表的一端進行操作;2、StaticStack 使用原生數組作爲內部存儲空間,它的最大容量由模板參數決定;3、鏈式棧的實現組合使用了單鏈表對象,在單鏈表的頭部進行操作能夠實現高效的入棧和出棧操作;4、棧“後進先出”的特性適用於檢測成對出現的符號;5、棧非常適合於需要“就近匹配”的場合。