二叉树的一种应用是无歧义地表示代数、关系或逻辑表达式。在上个世纪20年代初期,波兰的逻辑学家发明了一种命题逻辑的特殊表示方法,容许从公式中删除全部括号,称之为波兰表示法。可是,与原来带括号的公式相比,使用波兰表示法下降了公式的可读性,没有获得普遍的使用。在计算机出现后,这一表示法就颇有用了,特别是用于编写编译器和解释器。
以上公式必须借助括号,咱们才能理解到该公式首先须要计算出2-3和4+5的值,最后相乘才能得出结果。试想一下,若是没有括号,没有优先级的概念,对于2-3*4+5就会有多种理解方式,这就是所谓的歧义。前人为了不这种歧义,就创造了括号以及优先级的概念,可让咱们以惟一的方式来解读公式。可是,若是仅仅是为了不歧义,能够改变公式中使用符号的顺序,从而省略括号以及优先级的概念,更加的简练。这就是编译器所作的工做。编译器抛弃了一切对理解公式正确含义所没必要要的东西,以最简练的方式来表达公式。
以上公式若是抛弃括号以及优先级的概念,仅仅改变符号的顺序,能够这样表示:
公式中的操做符提早了,每一个操做符后面跟着两个操做数,从左向右遍历就能够获得惟一的计算步骤,就像这样:
根据就近原则,显然先计算A,再计算B,最后计算C。当咱们从左向右遍历的时候,每遇到一个操做符,它后面必然紧邻着两个相对应的操做数。也许有人会疑问,上图中
*号后面紧邻着
-号并非操做数,其实
-号表明着它会计算出一个临时的操做数tmp1做为
*号的第一个操做数。所以,咱们只须要把以上公式从左向右遍历一遍,就能知道该公式如何计算。编译器在将高级语言翻译成汇编代码时就是这么干的。
若是将操做符放在操做数的前面,能够获得一种不须要括号和优先级的表达方式,这就是波兰表达式。显然,波兰表达式很是简练,可是下降了公式的可读性,并不能一眼看出公式的结构,致使难以理解。与波兰表达式对应的还有一种表达式,那就是将操做符放在两个操做数的后面,称之为逆波兰表达式。根据操做符的位置,波兰表达式又被称之为先缀表达式,咱们平时使用的表达式称之为中缀表达式,逆波兰表达式称之为后缀表达式。
其中,先缀表达式与后缀表达式都是没有歧义的表达式,而中缀表达式若是不借助括号以及优先级会产生歧义,可是中缀表达式容易理解。由于中缀表达式中很容易看出基本计算单元,所谓基本计算单元指的是一个操做符加上两个操做数,这是计算的最小单位。
编译器须要将用户输入的公式转换成先缀表达式或后缀表达式,可是怎么作到呢?
答案是二叉树,怎么就从公式想到二叉树了呢?这就要说到基本计算单元了,在基本计算单元中确定有一个操做符来组织相关操做数,其次该基本计算单元的计算结果又多是另外一个基本计算单元的操做数。想一想二叉树中的节点有什么性质,节点既是一颗树的根节点,同时也是另外一棵树的子节点,因此基本计算单元不就能够当作一个根节点挂着两个子节点嘛。
将
(2-3)*(4+5)组织成二叉树看起来是这样:
以上的二叉树称之为表达式二叉树。表达式二叉树有些特性,全部的叶子节点都是操做数,全部的非叶子节点都是操做符。这很容易理解,在基本计算单元中,操做符是核心,同时计算结果是另外一个基本计算单元的操做数,反映到二叉树中,操做符既是子树的根节点同时也是另外一颗子树的子节点,那就是非叶子节点。
在以上表达式二叉树中,操做符是一棵树的根节点,左子树是该操做符的第一个操做数,右子树是该操做符的第二个操做数。还记得二叉树的先序、中序、后序遍历吗?不知道的看这里
数据结构与算法-二叉树遍历。先序就是先输出树的根节点其次是左子树最后是右子树,反映到公式中,不就是先输出操做符再输出第一个操做数最后是第二个操做数嘛。看来你想到了,表达式二叉树的先序遍历结果就是先缀表达式。同理,中序遍历是中缀表达式,后序遍历是后缀表达式。就像这样:
-
先序遍历: * - 2 3 + 4 5
-
中序遍历: 2 - 3 * 4 + 5
-
后序遍历: 2 3 - 4 5 + *
能够看到,若是将公式用表达式二叉树组织,那么先序就能够获取先缀表达式,中序就能够获取中缀表达式,后序就能够获取后缀表达式。可是,这里有个缺陷,中序遍历结果是没有考虑优先级以及括号的,因此结果是有歧义的。不过这不是问题,咱们能够经过判断来添加括号,这在后面探讨。
到目前为止,咱们已经探讨过什么是波兰表达式以及波兰表达式和表达式二叉树的关系,咱们也懂得能够经过表达式二叉树来获取先缀、中缀、后缀表达式。可是,咱们总不能每次看到中缀表达式都要经过画出二叉树来求解先缀以及后缀表达式吧,这里给出一我的工快速求解的方式。
为了快速求取先缀以及后缀表达式,咱们首先把括号补全,变成下面这样:
而后把全部操做符放在它所对应的左括号的前面,就是这样:
经过以上方式,咱们彻底能够心算出先缀以及后缀表达式,很是方便。
好了,如今的问题是如何经过先缀、中缀以及后缀表达式来构建表达式二叉树,这也能够当作3个问题,再加上如何正确输出中缀表达式,就是4个问题了。咱们来一一探讨。
老规矩,首先观察先缀表达式的特色,而后总结规律写出算法。
为告终构化观察上面公式,画出基本计算单元,就像这样:
看到了吗,若是以基本计算单元为核心,观察先缀表达式,这就是个栈。
咱们从左往右遍历先缀表达式,发现操做符就将其入栈,发现操做符的第二个操做数以后,将它们组织成最小的子树,而后操做符出栈,继续遍历下一个字符。在这个过程当中,操做数是不入栈的,栈里只有操做符,当操做符组织成最小计算单元以后就将其出栈。当栈空的时候,说明先缀表达式遍历完毕。
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();
}复制代码
中缀表达式获取二叉树的逻辑比较麻烦,由于括号以及优先级的处理让算法变得复杂。咱们能够从没有括号的简单的中缀表达式分析,假若有如下中缀表达式:
咱们在计算以上表达式时,首先计算
4 / 2的结果为
2,
2成了
*号的第二个操做数,而后计算
3 * 2的结果为
6,
6成了
+号的第二个操做数,最后计算
2 + 6得出结果为
8。
发现规律了吗,若是从右开始计算,每次计算结果都是下一个操做符的第二个操做数,那么遍历结束以后,结果就出来了。用代码实现能够用两个栈,一个栈保存从左到右的操做符,另外一个栈保存从左到右的操做数,就像这样:
而后咱们每次从操做符栈取出栈顶的操做符,再从操做数栈取出栈顶的两个操做数,将它们组成最小的子树,而后当作新的操做数压入到操做数栈中,重复上面的过程直到栈空,最终表达式二叉树构建出来了。
上面的中缀表达式太简单了,咱们换个更复杂的看看算法该如何改进,假若有如下中缀表达式:
若是还按照上面的算法来计算,最终计算成了
2 + 3 * ( 4 - 2 ),为何会这样呢?由于
*号的优先级高于
-号,应该先计算
*号再计算
-号,怎么处理呢?解决方法也很简单,咱们在将
-号压入栈的过程当中,发现
-号的优先级低于
*号。这时,将
*号弹出,同时将操做数栈顶的两个操做数弹出,组成最小子树压入操做数栈,最后变成这样:
很是完美,咱们只是对算法进行了小小的改动就能处理优先级的问题了,再接在励,如何处理括号呢?假若有如下中缀表达式:
发现了吗?其实括号也是优先级的问题,在上面的表达式中,
( 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;
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)
{
switch (pointerValue[0])
{
case '*':
case '/':
if (leftValue[0] == '+' || leftValue[0] == '-')
return true;
}
}
else if (leftOrRight == RIGHT)
{
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());
}复制代码
好了,到目前为止,关于表达式二叉树的内容已经探讨完毕。