數據結構與算法-表達式二叉樹

二叉樹的一種應用是無歧義地表示代數、關係或邏輯表達式。在上個世紀20年代初期,波蘭的邏輯學家發明了一種命題邏輯的特殊表示方法,容許從公式中刪除全部括號,稱之爲波蘭表示法。可是,與原來帶括號的公式相比,使用波蘭表示法下降了公式的可讀性,沒有獲得普遍的使用。在計算機出現後,這一表示法就頗有用了,特別是用於編寫編譯器和解釋器。
想要理解表達式二叉樹首先要理解波蘭表達式。
先從咱們熟悉的公式表達方法開始。
假如如今有一個數學公式: (2-3)*(4+5)
以上公式必須藉助括號,咱們才能理解到該公式首先須要計算出2-3和4+5的值,最後相乘才能得出結果。試想一下,若是沒有括號,沒有優先級的概念,對於2-3*4+5就會有多種理解方式,這就是所謂的歧義。前人爲了不這種歧義,就創造了括號以及優先級的概念,可讓咱們以惟一的方式來解讀公式。可是,若是僅僅是爲了不歧義,能夠改變公式中使用符號的順序,從而省略括號以及優先級的概念,更加的簡練。這就是編譯器所作的工做。編譯器拋棄了一切對理解公式正確含義所沒必要要的東西,以最簡練的方式來表達公式。
以上公式若是拋棄括號以及優先級的概念,僅僅改變符號的順序,能夠這樣表示:
*-23+45
公式中的操做符提早了,每一個操做符後面跟着兩個操做數,從左向右遍歷就能夠獲得惟一的計算步驟,就像這樣:
根據就近原則,顯然先計算A,再計算B,最後計算C。當咱們從左向右遍歷的時候,每遇到一個操做符,它後面必然緊鄰着兩個相對應的操做數。也許有人會疑問,上圖中 *號後面緊鄰着 -號並非操做數,其實 -號表明着它會計算出一個臨時的操做數tmp1做爲 *號的第一個操做數。所以,咱們只須要把以上公式從左向右遍歷一遍,就能知道該公式如何計算。編譯器在將高級語言翻譯成彙編代碼時就是這麼幹的。
若是將操做符放在操做數的前面,能夠獲得一種不須要括號和優先級的表達方式,這就是波蘭表達式。顯然,波蘭表達式很是簡練,可是下降了公式的可讀性,並不能一眼看出公式的結構,致使難以理解。與波蘭表達式對應的還有一種表達式,那就是將操做符放在兩個操做數的後面,稱之爲逆波蘭表達式。根據操做符的位置,波蘭表達式又被稱之爲先綴表達式,咱們平時使用的表達式稱之爲中綴表達式,逆波蘭表達式稱之爲後綴表達式。
其中,先綴表達式與後綴表達式都是沒有歧義的表達式,而中綴表達式若是不借助括號以及優先級會產生歧義,可是中綴表達式容易理解。由於中綴表達式中很容易看出基本計算單元,所謂基本計算單元指的是一個操做符加上兩個操做數,這是計算的最小單位。
編譯器須要將用戶輸入的公式轉換成先綴表達式或後綴表達式,可是怎麼作到呢?
答案是二叉樹,怎麼就從公式想到二叉樹了呢?這就要說到基本計算單元了,在基本計算單元中確定有一個操做符來組織相關操做數,其次該基本計算單元的計算結果又多是另外一個基本計算單元的操做數。想一想二叉樹中的節點有什麼性質,節點既是一顆樹的根節點,同時也是另外一棵樹的子節點,因此基本計算單元不就能夠當作一個根節點掛着兩個子節點嘛。
(2-3)*(4+5)組織成二叉樹看起來是這樣:
以上的二叉樹稱之爲表達式二叉樹。表達式二叉樹有些特性,全部的葉子節點都是操做數,全部的非葉子節點都是操做符。這很容易理解,在基本計算單元中,操做符是核心,同時計算結果是另外一個基本計算單元的操做數,反映到二叉樹中,操做符既是子樹的根節點同時也是另外一顆子樹的子節點,那就是非葉子節點。
在以上表達式二叉樹中,操做符是一棵樹的根節點,左子樹是該操做符的第一個操做數,右子樹是該操做符的第二個操做數。還記得二叉樹的先序、中序、後序遍歷嗎?不知道的看這裏 數據結構與算法-二叉樹遍歷。先序就是先輸出樹的根節點其次是左子樹最後是右子樹,反映到公式中,不就是先輸出操做符再輸出第一個操做數最後是第二個操做數嘛。看來你想到了,表達式二叉樹的先序遍歷結果就是先綴表達式。同理,中序遍歷是中綴表達式,後序遍歷是後綴表達式。就像這樣:
  • 先序遍歷: * - 2 3 + 4 5
  • 中序遍歷: 2 - 3 * 4 + 5
  • 後序遍歷: 2 3 - 4 5 + *
能夠看到,若是將公式用表達式二叉樹組織,那麼先序就能夠獲取先綴表達式,中序就能夠獲取中綴表達式,後序就能夠獲取後綴表達式。可是,這裏有個缺陷,中序遍歷結果是沒有考慮優先級以及括號的,因此結果是有歧義的。不過這不是問題,咱們能夠經過判斷來添加括號,這在後面探討。
到目前爲止,咱們已經探討過什麼是波蘭表達式以及波蘭表達式和表達式二叉樹的關係,咱們也懂得能夠經過表達式二叉樹來獲取先綴、中綴、後綴表達式。可是,咱們總不能每次看到中綴表達式都要經過畫出二叉樹來求解先綴以及後綴表達式吧,這裏給出一我的工快速求解的方式。
若是有如下中綴表達式:
(2-3)*(4+5)
爲了快速求取先綴以及後綴表達式,咱們首先把括號補全,變成下面這樣:
((2-3)*(4+5))
而後把全部操做符放在它所對應的左括號的前面,就是這樣:
*(-(2 3)+(4 5))
最後把括號去掉,變成這樣:
* - 2 3 + 4 5
這就是先綴表達式,同理能夠獲取後綴表達式。
經過以上方式,咱們徹底能夠心算出先綴以及後綴表達式,很是方便。
好了,如今的問題是如何經過先綴、中綴以及後綴表達式來構建表達式二叉樹,這也能夠當作3個問題,再加上如何正確輸出中綴表達式,就是4個問題了。咱們來一一探討。
  • 先綴表達式獲取二叉樹
老規矩,首先觀察先綴表達式的特色,而後總結規律寫出算法。
若是有如下先綴表達式:
* - 2 3 + 4 5
爲告終構化觀察上面公式,畫出基本計算單元,就像這樣:
看到了嗎,若是以基本計算單元爲核心,觀察先綴表達式,這就是個棧。
咱們從左往右遍歷先綴表達式,發現操做符就將其入棧,發現操做符的第二個操做數以後,將它們組織成最小的子樹,而後操做符出棧,繼續遍歷下一個字符。在這個過程當中,操做數是不入棧的,棧裏只有操做符,當操做符組織成最小計算單元以後就將其出棧。當棧空的時候,說明先綴表達式遍歷完畢。
代碼以下:
void ExpressionBinaryTree::buildBTreeByPreffixE()
{
	root = new BinaryTreeNode<string>();
	char c;
	cout << "->請輸入前綴表達式,以=結尾." << endl;
	cout << "->:";
	cin >> c;
	stack<BinaryTreeNode<string> *> parentStack;//用於保存存放父結點
	BinaryTreeNode<string> *pointer = root;//用於指向下一個保存數據的結點
	string blankStr = "";
	double tempDouble = 0;
	string tempStr;//用於輸入流,將浮點數轉換成字符串
	while (c != '=')
	{
		switch (c)
		{
		case '+':
		case '-':
		case '*':
		case '/':
			pointer->setValue(c + blankStr);//設置當前結點的值
			pointer->setLeftChild(new BinaryTreeNode<string>());//生成左結點
			parentStack.push(pointer);
			pointer = pointer->getLeftChild();
			break;
		}
		if (isdigit(c))
		{
			std::cin.putback(c);
			std::cin >> tempDouble;
			stringstream sss;
			sss << tempDouble;
			sss >> tempStr;
			pointer->setValue(tempStr);
			pointer = parentStack.top();
			while (pointer->getRightChild() != NULL)
			{
				parentStack.pop();//找到按前序遍歷的下一個結點
				if (parentStack.empty())
					return;
				pointer = parentStack.top();
			}
			pointer->setRightChild(new BinaryTreeNode<string>());//找到了按前序遍歷的下一個結點位置並生成結點
			pointer = pointer->getRightChild();
		}
		std::cin >> c;
	}
}複製代碼
  • 後綴表達式獲取二叉樹
後綴表達式獲取二叉樹的邏輯和上面的差很少,但也有幾點改變。首先,因爲操做符在操做數後面,在尋找基本計算單元的過程當中,將前兩個操做數入棧,在找到操做符以後,組織成最小的子樹,而後將操做數出棧便可。
代碼以下:
void ExpressionBinaryTree::buildBTreeBySuffixE()
{
	char c;
	cout << "->請輸入後綴表達式,以=結尾." << endl;
	cout << "->:";
	cin >> c;
	stack<BinaryTreeNode<string> *> opdStack;//抽象意義上爲操做數棧,但實際爲操做數和操做符構成的結點棧
	double tempDouble = 0;
	string tempStr;//用於輸入流,將浮點數轉換成字符串
	string blankStr = "";
	while (c != '=')
	{
		switch (c)
		{
		case '+':
		case '-':
		case '*':
		case '/':
			BinaryTreeNode<string> *secondOpd = opdStack.top();
			opdStack.pop();
			BinaryTreeNode<string> *firstOpd = opdStack.top();
			opdStack.pop();
			opdStack.push(new BinaryTreeNode<string>(c + blankStr, firstOpd, secondOpd));
			break;
		}
		if (isdigit(c))
		{
			std::cin.putback(c);
			std::cin >> tempDouble;
			stringstream sss;
			sss << tempDouble;
			sss >> tempStr;
			opdStack.push(new BinaryTreeNode<string>(tempStr));
		}
		std::cin >> c;
	}
	root = opdStack.top();//此時操做數棧中惟一元素即爲根元素
	opdStack.pop();
}複製代碼
  • 中綴表達式獲取二叉樹
中綴表達式獲取二叉樹的邏輯比較麻煩,由於括號以及優先級的處理讓算法變得複雜。咱們能夠從沒有括號的簡單的中綴表達式分析,假若有如下中綴表達式:
2 + 3 * 4 / 2
咱們在計算以上表達式時,首先計算 4 / 2的結果爲 22成了 *號的第二個操做數,而後計算 3 * 2的結果爲 66成了 +號的第二個操做數,最後計算 2 + 6得出結果爲 8
發現規律了嗎,若是從右開始計算,每次計算結果都是下一個操做符的第二個操做數,那麼遍歷結束以後,結果就出來了。用代碼實現能夠用兩個棧,一個棧保存從左到右的操做符,另外一個棧保存從左到右的操做數,就像這樣:
而後咱們每次從操做符棧取出棧頂的操做符,再從操做數棧取出棧頂的兩個操做數,將它們組成最小的子樹,而後當作新的操做數壓入到操做數棧中,重複上面的過程直到棧空,最終表達式二叉樹構建出來了。
上面的中綴表達式太簡單了,咱們換個更復雜的看看算法該如何改進,假若有如下中綴表達式:
2 + 3 * 4 - 2
若是還按照上面的算法來計算,最終計算成了 2 + 3 * ( 4 - 2 ),爲何會這樣呢?由於 *號的優先級高於 -號,應該先計算 *號再計算 -號,怎麼處理呢?解決方法也很簡單,咱們在將 -號壓入棧的過程當中,發現 -號的優先級低於 *號。這時,將 *號彈出,同時將操做數棧頂的兩個操做數彈出,組成最小子樹壓入操做數棧,最後變成這樣:
很是完美,咱們只是對算法進行了小小的改動就能處理優先級的問題了,再接在勵,如何處理括號呢?假若有如下中綴表達式:
2 + ( 4 - 2 ) * 3
發現了嗎?其實括號也是優先級的問題,在上面的表達式中, ( 4 -2 )的優先級比 *號還高,咱們在處理括號時按照處理優先級問題的邏輯就行,也就是說右括號的優先級是最高的。在壓入右括號的時候,不用看後面的操做符了,右括號就是最高的,應該直接將從左括號到右括號中的表達式組成子樹,而後壓入到操做數棧中,結果是這樣:
很是完美,咱們將括號問題轉化成優先級問題,很輕鬆的解決了該問題。到目前爲止,咱們已經解決了中綴表達式中優先級以及括號的問題,沒有更復雜的狀況了,目前的算法已經夠用了。
代碼以下:
//比較優先級
bool ExpressionBinaryTree::aIsGreaterOrEqualThanB(char a, char b)
{
	switch (a)
	{
	case '*':
	case '/':
		return true;
	case '+':
	case '-':
		if (b == '*' || b == '/')
			return false;
		return true;
	case '(':
		return false;
	}
	return false;
}

//中綴表達式轉換成二叉樹
void ExpressionBinaryTree::buildBTreeByInfixE()//構造中綴表達式二叉樹
{
	root = new BinaryTreeNode<string>();
	char c;
	cout << "->請輸入中綴表達式,以=結尾." << endl;
	cout << "->:";
	cin >> c;
	stack<BinaryTreeNode<string> *> opd;//操做數棧 //爲了方便統一管理,操做數和操做符所有定義爲string類型
	stack<string> opt;//操做符棧
	double tempDouble = 0;
	string tempStr;//用於輸入流,將浮點數轉換成字符串
	string blankStr = "";
	while (c != '=')
	{
		switch (c)
		{
		case '+':
		case '-':
		case '*':
		case '/':
			while (!opt.empty() && aIsGreaterOrEqualThanB(opt.top().c_str()[0], c))//若是棧頂操做符優先級高於讀入操做符優先級,則表名應該先計算棧頂操做符
			{
				BinaryTreeNode<string> *secondOpd = opd.top();
				opd.pop();
				BinaryTreeNode<string> *firstOpd = opd.top();
				opd.pop();//從操做數棧取出兩個操做數
				opd.push(new BinaryTreeNode<string>(opt.top(), firstOpd, secondOpd));//將操做數和操做符組成一個新結點存入棧中
				opt.pop();
			}
			opt.push(c + blankStr);//將讀入操做符入棧
			break;
		case '(':
			opt.push(c + blankStr);//遇到左括號直接入棧
			break;
		case ')':
			while (!opd.empty() && opt.top().c_str()[0] != '(')//爲了防止冗贅括號,但未檢測括號不匹配
			{
				BinaryTreeNode<string> *secondOpd = opd.top();
				opd.pop();
				BinaryTreeNode<string> *firstOpd = opd.top();
				opd.pop();//從操做數棧取出兩個操做數
				opd.push(new BinaryTreeNode<string>(opt.top(), firstOpd, secondOpd));//將操做數和操做符組成一個新結點存入棧中
				opt.pop();
			}
			opt.pop();//將左括號出棧
			break;
		}
		if (isdigit(c))
		{
			std::cin.putback(c);
			std::cin >> tempDouble;
			stringstream sss;
			sss << tempDouble;
			sss >> tempStr;
			opd.push(new BinaryTreeNode<string>(tempStr));
		}
		std::cin >> c;
	}
	while (!opt.empty())
	{
		BinaryTreeNode<string> *secondOpd = opd.top();
		opd.pop();
		BinaryTreeNode<string> *firstOpd = opd.top();
		opd.pop();//從操做數棧取出兩個操做數
		opd.push(new BinaryTreeNode<string>(opt.top(), firstOpd, secondOpd));//將操做數和操做符組成一個新結點存入棧中
		opt.pop();
	}
	root = opd.top();//此時操做數棧中惟一元素即爲根元素
	opd.pop();
}複製代碼
  • 正確輸出中綴表達式
還有最後一個問題,在中序遍歷表達式二叉樹時,如何正確的輸出括號?
咱們使用遞歸方式輸出中序遍歷結果,在整個過程當中只涉及到3個節點,分別是根節點、左子樹以及右子樹。
正確輸出括號須要分類討論。好比說:
一、若是根節點是 +號,那麼不管左子樹以及右子樹是什麼操做符,它們都是不須要加括號的,由於根節點 +號是最小優先級的
二、若是根節點是 -號,那麼只有右子樹是 +號或者 -號時,右子樹才須要加括號
三、若是根節點是 *號,那麼只有左子樹或右子樹是 +號或者 -號時,它們才須要加括號
四、若是根節點是 /號,那麼若是左子樹或右子樹是 +號或者 -號時,它們須要加括號,其次,若是右子樹是 *號或者 /號時,右子樹也須要加括號
以上是全部須要加括號的狀況,咱們只須要在遍歷左子樹或者右子樹以前判斷一下,就知道是否加括號了。
代碼以下:
//是否應該輸出括號
bool ExpressionBinaryTree::shouldPrintBracket(BinaryTreeNode<string> *pointer, int leftOrRight)
{
	if (pointer == NULL)
		return false;
	BinaryTreeNode<string> *left = pointer->getLeftChild();
	BinaryTreeNode<string> *right = pointer->getRightChild();
	if (left == NULL || right == NULL)
		return false;
	string pointerValue = pointer->getValue();
	string leftValue = left->getValue();
	string rightValue = right->getValue();
	if (leftOrRight == LEFT)//若是pointer是左結點
	{
		switch (pointerValue[0])
		{
		case '*':
		case '/':
			if (leftValue[0] == '+' || leftValue[0] == '-')
				return true;
		}
	}
	else if (leftOrRight == RIGHT)//若是pointer是右結點
	{
		switch (pointerValue[0])
		{
		case '*':
			if (rightValue[0] == '+' || rightValue[0] == '-')
				return true;
			break;
		case '/':
			if (rightValue[0] == '+' || rightValue[0] == '-' || rightValue[0] == '*' || rightValue[0] == '/')
				return true;
			break;
		case '-':
			if (rightValue[0] == '+' || rightValue[0] == '-')
				return true;
			break;
		}
	}
	return false;
}

void ExpressionBinaryTree::recursionPrintInE(BinaryTreeNode<string> * root)//遞歸調用打印後綴表達式
{
	if (root == NULL)
		return;
	if (shouldPrintBracket(root, LEFT)){
		cout << "( ";
		recursionPrintInE(root->getLeftChild());
		cout << ") ";
	}
	else
		recursionPrintInE(root->getLeftChild());
	cout << root->getValue() << " ";
	if (shouldPrintBracket(root, RIGHT)){
		cout << "( ";
		recursionPrintInE(root->getRightChild());
		cout << ") ";
	}
	else
		recursionPrintInE(root->getRightChild());
}複製代碼
好了,到目前爲止,關於表達式二叉樹的內容已經探討完畢。
更多內容期待讀者在實踐中積累。
若是以爲有所收穫,但願關注筆者~
相關文章
相關標籤/搜索