数据结构与算法-表达式二叉树

二叉树的一种应用是无歧义地表示代数、关系或逻辑表达式。在上个世纪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());
}复制代码
好了,到目前为止,关于表达式二叉树的内容已经探讨完毕。
更多内容期待读者在实践中积累。
若是以为有所收获,但愿关注笔者~
相关文章
相关标签/搜索