1. 程式人生 > >資料結構之-鏈式棧及其常見應用(進位制轉換、括號匹配、行編輯程式、表示式求值等)

資料結構之-鏈式棧及其常見應用(進位制轉換、括號匹配、行編輯程式、表示式求值等)

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做了幾個簡易的計算器,可以實現加減乘除、指數、開方等運算,支援連續輸入。有需要的可以自行下載,以供相互交流,計算機的介面如下:

計算器程式碼下載地址:

MFC+棧實現簡易計算器