資料結構之-鏈式棧及其常見應用(進位制轉換、括號匹配、行編輯程式、表示式求值等)
1、棧的概念
棧(stack)又名堆疊,它是一種運算受限的線性表。其限制是僅允許在表的一端進行插入和刪除運算。這一端被稱為棧頂,相對地,把另一端稱為棧底。向一個棧插入新元素又稱作進棧、入棧或壓棧,它是把新元素放到棧頂元素的上面,使之成為新的棧頂元素;從一個棧刪除元素又稱作出棧或退棧,它是把棧頂元素刪除掉,使其相鄰的元素成為新的棧頂元素。總的來說就是LIFO(Last In First Out);
程式碼:
#pragma once /* *Copyright© 中國地質大學(武漢) 資訊工程學院 *All right reserved. * *檔名稱:Stack.h *摘 要:編寫棧演算法 * *當前版本:1.0 *作 者:邵玉勝 *完成日期:2018-12-28 */ #ifndef STACK_H_ #define STACK_H_ #include<iostream> //結點結構體,雙向棧 template<class T> struct StackNode { T _tData; //資料域 StackNode<T>* _pNext; //指標域,指向下一結點 StackNode<T>* _pLast; //指標域,指向上一結點(為了行編輯器函式的實現) StackNode(StackNode<T>* next = nullptr, StackNode<T>* last = nullptr) { //用指標建構函式 this->_pNext = nullptr; this->_pLast = nullptr; } StackNode(T data, StackNode<T>* next = nullptr, StackNode<T>* last = nullptr) { //用指標建構函式 this->_tData = data; this->_pNext = next; this->_pLast = last; } }; template<class T> class Stack { private: StackNode<T>* _pTop; //棧頂指標 StackNode<T>* _pBottom; //棧底指標,為了方便行編輯器使用 int _iConuntOfElement; //結點數量 public: Stack(); //建構函式 Stack(Stack<T>& copy); //建構函式 ~Stack(); //解構函式 bool IsEmpty(); //判斷棧是否為空 void MakeEmpty(); //將棧中的元素全部刪除 void Put(const T data); //頂端插入資料 int Size() { return _iConuntOfElement; } //返回棧中的結點數 void GetTop(T& data); //獲取頂端結點 void Pop(T& data); //頂端彈出結點,並將元素傳至引數中 void Traverse(); //逆序棧中的結點 void DisPlay(bool forward = true); //輸出函式,預設正向輸出 }; //建構函式,為棧頂和棧底分配記憶體 template<class T> Stack<T>::Stack() { _pTop = _pBottom = nullptr; this->_iConuntOfElement = 0; } //拷貝建構函式 //缺少此函式,在傳參與析構的時候容易出問題 template<class T> Stack<T>::Stack(Stack<T>& copy) { StackNode<T>* pCur = this->_pTop; //建立指標,用於遍歷本物件中的結點 while (pCur) { //遍歷本物件的結點 T data = pCur->_tData; //依此取出結點值 copy.Put(data); //插入到copy棧中 pCur = pCur->_pNext; } } //解構函式 template<class T> Stack<T>::~Stack() { MakeEmpty(); //釋放結點記憶體 this->_pTop = this->_pBottom = nullptr; //將指標指向空,避免出現野指標 } //判斷棧是否為空 template<class T> bool Stack<T>::IsEmpty() { if (!this->_pTop) //如果棧物件沒有頭節點,那麼棧就為空 return true; return false; } //將棧中的元素全部刪除 template<class T> void Stack<T>::MakeEmpty(){ StackNode<T>* pDel = nullptr; //建立臨時結點指標,用於釋放結點記憶體 while (_pTop) { //迴圈依此從頂端刪除 pDel = this->_pTop; this->_pTop = _pTop->_pNext; delete pDel; } _iConuntOfElement = 0; //棧結點數量置零 } //頂端插入資料 template<class T> void Stack<T>::Put(const T data) { StackNode<T>* newData = new StackNode<T>(data); //為新結點分配記憶體,新結點的並確定結點指標指向 if (!newData) { //如果記憶體分配錯誤 std::cerr << "記憶體分配錯誤!" << std::endl; exit(-1); } if (this->IsEmpty()) { //如果插入的是第一個結點 newData->_pLast = nullptr; //將這個結點的指向上一下一結點的指標都賦空值 newData->_pNext = nullptr; this->_pTop = newData; //將棧頂和棧底指標都指向這個結點 this->_pBottom = newData; ++this->_iConuntOfElement; //節點數加1 return; } this->_pTop->_pLast = newData; //棧頂的上一結點指標指向新結點 newData->_pNext = this->_pTop; //將新結點的下一結點指標指向棧頂 this->_pTop = newData; //新結點作為棧頂 ++this->_iConuntOfElement; } //獲取頂端結點 template<class T> void Stack<T>::GetTop(T& data) { if (this->IsEmpty()) //棧為空,直接返回 return; data = this->_pTop->_tData; //獲取棧頂元素,賦值給返回引數 return; } //頂端彈出元素,並將元素傳至引數中 template<class T> void Stack<T>::Pop(T& data) { if (this->IsEmpty()) //棧為空,直接返回 return; data = this->_pTop->_tData; //先取出棧頂的值 StackNode<T>* pDel = this->_pTop; if (this->_pTop->_pNext) { //如果有後繼結點,就將後繼結點的上一個指標指向空 this->_pTop->_pNext->_pLast = nullptr; this->_pTop = this->_pTop->_pNext; delete pDel; //釋放原棧頂記憶體 --this->_iConuntOfElement; //棧結點數量遞減 return; } delete pDel; //如果就只有一個結點,直接釋放原棧頂的空間 this->_pTop = this->_pBottom = nullptr; //沒有後繼結點,釋放棧頂空間,將指標指向空,避免出現野指標 --this->_iConuntOfElement; //棧結點數量遞減 return; } //逆序棧中的結點 template<class T> void Stack<T>::Traverse() { StackNode<T>* pCur = this->_pTop; StackNode<T>* pTmp = nullptr; //臨時指標,迴圈要用 while (pCur) { //迴圈將棧中結點的前驅與後繼指標對調 pTmp = pCur->_pLast; pCur->_pLast = pCur->_pNext; pCur->_pNext = pTmp; pCur = pCur->_pLast; //這個是很值得細細品味的 } //將棧頂與棧頂指標對調 pTmp = this->_pTop; this->_pTop = this->_pBottom; this->_pBottom = pTmp; return; } //輸出函式 template<class T> void Stack<T>::DisPlay(bool forward = true) { StackNode<T>* pCur; if(forward == true) pCur = this->_pTop; else pCur = this->_pBottom; while (pCur) { //如果當前指標不為空,就一直迴圈遍歷 std::cout << pCur->_tData; //為了照顧 //if (iCount % 10 == 0) //每隔十個換一行,以免輸出的太長 // std::cout << std::endl; if (forward == true) pCur = pCur->_pNext; else pCur = pCur->_pLast; } std::cout << std::endl; } #endif // !STACK_H_
由於棧的後進先出的特性,使得棧有很多的應用,下面就一一舉例:
2、棧的應用之-數制轉換
十進位制數N和其他d進位制數的轉換是計算機實現計算的基本問題,其解決方法很多,其中一個簡單演算法基於下列原理:
N = (N div d) * d + N mod d(其中:div為整除運算,mod為求餘運算)
例如:將十進位制的121轉化為二進位制的過程為:
程式碼:
//進位制轉換函式 //十進位制轉換為其他低進位制,如二進位制,八進位制, 預設是二進位制 //注意這個函式僅適用於整數 void DecConvert(Stack<int>& result, const int decimal, const int radix) { if (radix < 2 && radix > 10) { //先判斷引數合不合理 std::cout << "進位制轉換函式引數不合理!" << std::endl; return; } result.MakeEmpty(); //先將用於結果返回的棧引數置空 int iQuotient = decimal; //商 int iRemainder; //餘數 while (iQuotient) { //商為0的時候結束迴圈 iRemainder = iQuotient % radix; //求餘 result.Put(iRemainder); //將餘數裝入結果的棧中 iQuotient /= radix; //求商 } }
3、棧的應用之-括號匹配
假設表示式中包含三種括號:圓括號、方括號和花括號,並且它們可以任意巢狀。例如{[()]()[{}]}或[{()}([])]等為正確格式,而{[}()]或[({)]為不正確的格式。那麼怎麼檢測表示式是否正確呢?
這個問題可以用“期待的急迫程度”這個概念來描述。對錶達式中的每一個左括號都期待一個相應的右括號與之匹配,表示式中越遲出現並且沒有得到匹配的左括號期待匹配的程度越高。不是期待出現的右括號則是不正確的。它具有天然的後進先出的特點。
於是我們可以設計演算法:演算法需要一個棧,在讀入字元的過程中,如果是左括號,則直接入棧,等待相匹配的同類右括號;如果是右括號,且與當前棧頂左括號匹配,則將棧頂左括號出棧,如果不匹配則屬於不合法的情況。另外,如果碰到一個右括號,而堆疊為空,說明沒有左括號與之匹配,則非法。那麼,當字元讀完的時候,如果是表示式合法,棧應該是空的,如果棧非空,那麼則說明存在左括號沒有相應的右括號與之匹配,也是非法的情況。
程式碼:
//括號匹配函式
//注意此處的括號匹配使用的時英文括號
//檢驗括號是否匹配,該方法使用“期待的緊迫程度”這個概念來描述的
//可能出現不匹配的情況
//1、到來的有括弧非是所“期待”的
//2、到來的是不速之客(左括弧多了)
//3、直到結束也沒有到來所“期待”的
//設計思想:1、凡是出現左括弧就讓他進棧
//2、若出現右括弧,則檢查棧是否為空,若為空,就表明右括弧多了
//否則和棧頂元素匹配,匹配成果,則左括弧出棧,否則,匹配失敗!
//3、表示式檢驗結束時,檢查棧是否為空,不為空,說明左括號多了
void ParMatching(std::string str) {
//先定義一些括號常量
const char LCURVES = '(';
const char RCURVES = ')';
const char LBRAKET = '[';
const char RBRAKET = ']';
const char LBRACE = '{';
const char RBRACE = '}';
Stack<char> stackMatch; //定義一個棧物件,用於裝入彈出左括號
char chPop; //儲存彈出的括號
for (int i = 0; i < str.length(); i++) {
switch (str[i])
{
case LCURVES: //凡是出現左括弧就讓他進棧
case LBRAKET:
case LBRACE:
stackMatch.Put(str[i]);
break;
case RCURVES: //若出現右括弧
if (!stackMatch.IsEmpty()) { //則檢查棧是否為空,否則和棧頂元素匹配
stackMatch.GetTop(chPop);
if (chPop == LCURVES) { //匹配成果,則左括弧出棧
stackMatch.Pop(chPop);
std::cout << "()匹配成功!\n";
break;
}
}
std::cout << ")匹配失敗!\n"; //否則,就表明右括弧多了
break;
case RBRAKET:
if (!stackMatch.IsEmpty()) {
stackMatch.GetTop(chPop);
if (chPop == LBRAKET) {
stackMatch.Pop(chPop); //彈出
std::cout << "[]匹配成功!\n";
break;
}
}
std::cout << "]匹配失敗!\n";
break;
case RBRACE:
if (!stackMatch.IsEmpty()) {
stackMatch.GetTop(chPop);
if (chPop == LBRACE) {
stackMatch.Pop(chPop); //彈出
std::cout << "{}匹配成功!\n";
break;
}
}
std::cout << "}匹配失敗!\n";
break;
default:
break;
}
}
//將棧中沒有匹配的左括號給彈出來
for (int i = 0; i < stackMatch.Size(); i++) {
stackMatch.Pop(chPop);
std::cout << chPop << "匹配失敗!\n";
}
return;
}
4、棧的應用之-行編輯程式
一個簡單的行編輯程式的功能是:接受使用者從終端輸入的程式或資料,並存入使用者的資料區。由於使用者在終端上進行輸入時,不能保證不出差錯,因此,若在行編輯程式中“每接受一個字元即存入使用者區”的做法顯然是不恰當的。
較好的做法是,設立一個輸入緩衝區,用以接收使用者輸入的一行字元,然後逐行存入使用者資料區。允許使用者輸入出差錯,
並在發現有誤時及時改正。
例如:當用戶發現剛剛建入的一個字元是錯的時,可補進一個退格符“#”,以表示前一個字元無效;如果發現當前鍵入的行內差錯較多或難以補救,則可以輸入一個退格符“@”,以表示當前行中的字元均無效。例如,假設從終端接受了這兩行字元:
whil##ilr#e(s#*s)
[email protected](*s=#++)
則實際有效的是下列兩行:
while(*s)
putchar(*s++)
程式碼:
//行編輯程式問題
//設立一個輸入緩衝區,用以接受使用者輸入的一行字元,
//然後逐行存入使用者資料區。允許使用者輸入出差錯,並在發現的時候可以及時改正。
//例如,當用戶剛剛鍵入的一個字元是錯誤的時候,可以補進一個退格符“#”,
//以表示前一個字元無效;如果發現當前鍵入的行內差錯比較多或難以補救,
//則可以鍵入一個退行符“@”, 以表示當前行中的字元均無效。
void LineEditing() {
Stack<char> stackResult; //建立臨時棧,用於儲存輸入字元的快取
char chInput; //接收輸入的字元
char chTmp; //臨時字元變數,用於儲存彈出的字元
chInput = getchar(); //獲取輸入的字元
while (chInput != EOF) { //EOF為全文結束符,遇到他則不在等待輸入,ctrl+Z
stackResult.MakeEmpty();
while (chInput != EOF && chInput != '\n') { //如果沒有輸入回車和全文結束符
switch (chInput)
{
case '#': //輸錯一個字元
stackResult.Pop(chTmp); //彈出這個字元
break;
case '@': //輸出一行字元
stackResult.MakeEmpty(); //情況棧
break;
default:
stackResult.Put(chInput);
break;
}
chInput = getchar();
}
stackResult.DisPlay(false);
chInput = getchar();
}
}
5、棧的應用之表示式求值
表示式求值可以認為是棧的典型應用之一了,如果想弄明白表示式求值的方法這裡一兩句是介紹不清楚的,主要需要了解表示式三種表示方法(中綴、字首、字尾)以及為什麼要選後綴最為最後參與運算的表示方式(讀者可以自行了解,這裡不做介紹)。既然是用字尾作為週會殘雲運算的表示方式,而我們平時使用的右採用的中綴表示式,因此,欲求中綴表示式的值,需要將中綴表示式的值轉換為字尾表示式,我將主要的參考程式碼放下下面,裡面由較為詳細的註釋說明。
程式碼:
//輔助函式:運算子優先順序函式,返回符號的優先順序(+-x/()六種)
int OperatorPriority(char chOperator) {
int result;
switch (chOperator)
{
case '(': //左括號的優先順序最大為6
result = 6;
break;
case ')': //右括號的優先順序最小,為1
result = 1;
break;
case '+': //+-的優先順序為2
case '-':
result = 2;
break;
case '*': //乘除的優先順序為3
case '/':
result = 3;
break;
case '#': //棧底放一個‘#’方便運算,比所有運算子都笑
result = 0;
break;
default:
result = -1; //如果是除了數字與上訴運算子的其他字元,就預設返回-1
break;
}
return result;
}
//1、先將原表示式變成字尾表示式
//2、在利用棧將字尾表示式求值
float ExpressionEvaluating(std::string strExpression) {
Stack<char> stkChOperator; //建立一個用於裝運算子字元的棧
Stack<char> stkChPostfix; //建立一個用於裝字尾表示式的棧
Stack<float> stkReult; //儲存運算結果的棧
char chTopOfStack; //運算子棧的頂端操作符
float fReault; //儲存結果
stkChOperator.Put('#'); //往棧底放入‘#’,其比任何運算子的優先順序都小
for (int i = 0; i < strExpression.length(); i++) {//對字串中的字元進行分析
char chRead = strExpression[i];
if (chRead >= '0' && chRead <= '9') //如果讀出的是數字,直接放到字尾棧
stkChPostfix.Put(chRead);
else //讀出的是其他字元的話
{
if (OperatorPriority(chRead) == -1) {//如果讀出的不是數字,也不是操作符
std::cerr << "表示式有誤!\n";
exit(-1);
}
while (true) {
stkChOperator.GetTop(chTopOfStack);
if (OperatorPriority(chTopOfStack) >=
OperatorPriority(chRead)) {
stkChOperator.Pop(chTopOfStack); //彈出頂端操作符
if (chTopOfStack != '(') //'('是不入字尾表示式的棧的
stkChPostfix.Put(chTopOfStack); //向後綴表示式的棧讀入運算子棧頂端操作符
}
else
break;
}
if (chRead != ')') //')'也是不吐字尾表示式的棧的
stkChOperator.Put(chRead); //向運算子棧中裝入讀取的操作符
}
}
while (stkChOperator.Size() > 1) { //將操作符棧中的剩餘操作符彈出放入字尾表示式
stkChOperator.Pop(chTopOfStack);
stkChPostfix.Put(chTopOfStack);
}
stkChPostfix.Traverse(); //將字尾表示式棧逆序
//開始由字尾表示式計算結果
while (stkChPostfix.Size() > 0) { //一直彈出字尾表示式棧頂值
stkChPostfix.Pop(chTopOfStack);
if (chTopOfStack >= '0' && chTopOfStack <= '9')
stkReult.Put(float(chTopOfStack) - 48.0); //0-9的ascii碼是 48-58
else //如果彈出的是運算結果
{
float fFirst;
float fSencond;
stkReult.Pop(fSencond);
stkReult.Pop(fFirst);
switch (chTopOfStack)
{
case '+':
stkReult.Put(fFirst + fSencond);
break;
case '-':
stkReult.Put(fFirst - fSencond);
break;
case '*':
stkReult.Put(fFirst * fSencond);
break;
case '/':
stkReult.Put(fFirst / fSencond);
break;
default:
break;
}
}
}
stkReult.GetTop(fReault); //獲取結果
return fReault; //返回結果
}
此外,本人還利用棧和MFC做了幾個簡易的計算器,可以實現加減乘除、指數、開方等運算,支援連續輸入。有需要的可以自行下載,以供相互交流,計算機的介面如下:
計算器程式碼下載地址: