前言:二叉树的一种应用是无歧义地表示代数、关系或逻辑表达式。在上个世纪20年代初期,波兰的逻辑学家发明了一种命题逻辑的特殊表示方法,允许从公式中删除所有括号,称之为波兰表示法。但是,与原来带括号的公式相比,使用波兰表示法降低了公式的可读性,没有得到广泛的使用。在计算机出现后,这一表示法就很有用了,特别是用于编写编译器和解释器。用户输入的待求表达式,也就是中缀表达式,对于人来说,这个很好理解,但是对于计算机,后缀表达式求值更加容易。如果在计算机中需要对算术表达式求值,我们可以先把中缀表达式转换成后缀表达式,再对后缀表达式用栈进行求值。
1、算术表达式
即由数字和运算符组成的算式,如(2-3)*(4+5)。其中,二元运算符总是置于与之相关的两个运算对象之间,这种表示法也称为中缀表达示。对于人们来说,这是最直观的一种求值方式,先算括号里的。
以上公式必须借助括号,我们才能理解到该公式首先需要计算出2-3和4+5的值,最后相乘才能得出结果。试想一下,如果没有括号,没有优先级的概念,对于2-3*4+5就会有多种理解方式,这就是所谓的歧义。前人为了避免这种歧义,就创造了括号以及优先级的概念,可以让我们以唯一的方式来解读公式。但对于一个如上所示的算术表达式来说,如果让人来计算的话,是很简单的,可是让计算机来计算这样的算术表达式就有点难了,计算机难以计算这样的算式是因为计算机的内存模型多用栈,或者说计算机只能顺序操作,这样就无法很好处理运算符的优先级问题。所以要把中序表达式转化成计算机容易理解的形式——即后缀表达式(又称逆波兰表达式)。
2、波兰表达式
以上公式如果抛弃括号以及优先级的概念,仅仅改变符号的顺序:*-23+45。可以发现表达式中没有括号了,在这样的表示中可以不用括号即可确定求值的顺序,也就是说计算机处理这个表达式的时候就不需要考虑优先级的问题了。
公式中的操作符提前了,每个操作符后面跟着两个操作数,从左向右遍历就可以得到唯一的计算步骤,就像这样:
根据就近原则,显然先计算A,再计算B,最后计算C。当我们从左向右遍历的时候,每遇到一个操作符,它后面必然紧邻着两个相对应的操作数。也许有人会疑问,上图中*号后面紧邻着-号并不是操作数,其实-号代表着它会计算出一个临时的操作数tmp1作为*号的第一个操作数。因此,我们只需要把以上公式从左向右遍历一遍,就能知道该公式如何计算。编译器在将高级语言翻译成汇编代码时就是这么干的。*-23+45此表达式将操作符放在操作数的前面,可以得到一种不需要括号和优先级的表达方式,这就是波兰表达式。显然,波兰表达式非常简练,可以不用括号就能知道求值顺序,但是降低了公式的可读性,并不能一眼看出公式的结构,导致难以理解。
3、逆波兰表达式
与波兰表达式对应的还有一种表达式,那就是将操作符放在两个操作数的后面,称之为逆波兰表达式。根据操作符的位置,波兰表达式又被称之为先缀表达式,我们平时使用的表达式称之为中缀表达式,逆波兰表达式称之为后缀表达式。
逆波兰表达式又叫做后缀表达式,是一种十分有用的表达式,它将复杂表达式转换为可以依靠简单的操作得到计算结果的表达式。例如(a+b)*(c+d)转换为ab+cd+*。
它的优势在于只用两种简单操作,入栈和出栈就可以搞定任何普通表达式的运算。其运算方式如下:
如果当前字符为变量或者为数字,则压栈,如果是运算符,则将栈顶两个元素弹出作相应运算,结果再入栈,最后当表达式扫描完后,栈里的就是结果。
对于计算机,后缀表达式比前缀表达式求值更容易。所以对于算数表达式的计算:我们需要把中缀表达式转为后缀表达式,再对后缀表达式求值即可。
对于算术表达式(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
这就是先缀表达式,同理可以获取后缀表达式。
通过以上方式,我们完全可以心算出先缀以及后缀表达式,非常方便。
为了简单,此处代码的范围仅限于加减乘除四则运算,而且操作数范围为0-9。如果操作数范围超过了10,可以把算术表达式String改为ArrayList
算术表达式将+、-、*、/四个基本运算符号分成两个等级,+、-优先级低,*、/优先级高。
1、中缀表达式转后缀表达式
需要用到两个数据结构:
①后缀表达式队列:postfixQueue,用于存储逆波兰表达式(其实不用队列排序直接输出也行)
②操作符栈:opStack,对用户输入的操作符进行处理,用于存储运算符
算法思想:
从左向右依次读取算术表达式的元素X,分以下情况进行不同的处理:
(1)如果X是操作数,直接入队
(2)如果X是运算符,再分以下情况:
a)如果栈为空,直接入栈。
b)如果X==”(“,直接入栈。
c)如果X==”)“,则将栈里的元素逐个出栈,并入队到逆波兰式队列中,直到第一个配对的”(”出栈。(注:“(”和“)”都不 入队)
d)如果是其他操作符(+ - * /),则和栈顶元素进行比较优先级。 如果栈顶元素的优先级大于等于X,则出栈并把栈中 弹出的元素入队,直到栈顶元素的优先级小于X或者栈为空。弹出完这些元素后,才将遇到的操作符压入到栈中。
(3)最后将栈中剩余的操作符全部入队
当读取完中序表达式后,转化好的逆波兰式就存储在队列里了。
Java代码:
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Stack;
import javax.lang.model.element.Element;
import javax.xml.transform.Templates;
/*
* 中缀表达式转后缀表达式,并对后缀表达式求值
* 即实现计算机中算术表达式的求值
*/
public class InfixToPostfix
{
/*
* 中缀转后缀
*/
public Queue inToPost()
{
String expression = "(2-3)*(4+5)";
Queue postfixQueue = new LinkedList<>();//存储转化后的逆波兰式
Stack opStack = new Stack<>();//存储操作数的栈
//依次读入用户输入的中缀表达式的元素c
for(Character c:expression.toCharArray())
{
// System.out.println(c);
//如果读入的字符是数字,则直接入队
if(Character.isDigit(c))
{
postfixQueue.offer(c);
}
if(isOperator(c))
{
if(c=='(')
opStack.push(c);//如果操作符为 ( 直接入栈
else
{
//如果操作符为 ) ,依次出栈并入队到逆波兰式队列中,直到遇到第一个(
if(c==')')
{
while(!opStack.isEmpty())
{
Character temp = opStack.peek();
if(temp=='(')
{
opStack.pop();
break;
}
else
postfixQueue.offer(opStack.pop());
}
}
else
{
//如果是其他操作符(+ - * /),则和栈顶元素进行比较优先级
while(!opStack.isEmpty())
{
Character temp1 = opStack.peek();
boolean isPop = isGreat(temp1,c);
if(isPop)
{
postfixQueue.offer(opStack.pop());
}
else
break;
}
opStack.push(c);//这儿其实就包括栈为空,直接进栈的情况
}
}
}
}
// 将剩余的操作符入队
while(!opStack.isEmpty())
postfixQueue.offer(opStack.pop());
return postfixQueue;
}
/*
* 判断是否是操作符,还可以防止空格
*/
private boolean isOperator(Character c)
{
if(c=='('||c==')'||c=='+'||c=='-'||c=='*'||c=='/')
return true;
else
return false;
}
/*
* 得到操作符的优先级
*/
private int getPriority(Character c)
{
if(c=='+'||c=='-')
return 1;
else
if(c=='*'||c=='/')
return 2;
else
return 0;//表示左或右括号
}
/*
* 比较op1和op2的优先级大小,如果op1>=op2,返回true,如果op1=getPriority(op2))
return true;
else
return false;
}
public static void main(String[] args)
{
Queue postfixQueue = new InfixToPostfix().inToPost();
System.out.print("中缀转后缀:");
for(Character temp:postfixQueue)
System.out.print(temp);
}
}
输出结果:
算术表达式:(2-3)*(4+5)
中缀转后缀:23-45+*
2、后缀表达式求值
需要用到一个结果栈stack:用于存放计算的中间过程的值和最终结果
算法思想:
计算一个逆波兰式是非常简单的,只需要遵循以下步骤。
首先准备一个栈stack.
1、从左开始向右遍历逆波兰式的元素。
2、如果取到的元素是操作数,直接入栈stack,如果是运算符,从栈中弹出2个数进行运算,然后把运算结果入栈
当遍历完逆波兰式时,计算结果就保存在栈里了。
Java代码:
/*
* 后缀表达式求值
*/
public Stack postfixCalculate(Queue postfixQueue)
{
Stack stack = new Stack<>();
//当遍历完逆波兰式时,计算结果就保存在栈里了
for(Character temp:postfixQueue)
{
if(Character.isDigit(temp))
{
//把字符转化为其表示的整数,不能直接int num = temp,因为这样只是返回了字符的Ascii码,0-9的Ascii码为48-57
int num = temp-48;
stack.push(num);//如果是操作数直接入栈
}
else
{
//如果是运算符,从栈中弹出2个数进行运算,然后把运算结果入栈
int c2 = stack.pop();//后一个操作数
int c1 = stack.pop();//前一个操作数
switch (temp)
{
case '+':
int a=c1+c2;
stack.push(a);
break;
case '-':
int b = c1-c2;
stack.push(b);
break;
case '*':
int c = c1*c2;
stack.push(c);
break;
case '/':
int d = c1/c2;
stack.push(d);
break;
}
}
}
return stack;
}
输出结果:
后缀表达式求值:-9
1、算术表达式转化为二叉树
需要数据结构:二叉树节点栈nodeStack,用于存放所有临时节点和最后根节点
对逆波兰式的计算步骤稍作改变,我们就可以将一个算式转化成二叉树,具体步骤如下:
首先准备一个二叉树节点栈s.
①从左开始向右遍历逆波兰式的元素。
②新建一个树节点p,值为当前元素的值,如果取到的元素是操作数,直接把p入栈s,如果是运算符,从栈中弹出2个节点,把第一个弹出的节点作为p的右子树,第二个弹出的节点作为p的左子树,然后把p入栈。
当遍历完逆波兰式时,树的根节点就保存在栈里了。
Java代码:
/*
* 后缀表达式转二叉树:对逆波兰式的计算步骤稍作改变,我们就可以将一个算式转化成二叉树
*/
public BinaryTreeNode postfixToTree(Queue postfixQueue)
{
Stack> nodeStack = new Stack<>();
// 将逆波兰式转化成二叉树:当遍历完逆波兰式时,树的根节点就保存在栈里了。
while(!postfixQueue.isEmpty())
{
Character c= postfixQueue.poll();
// 以当前的元素的值新建一个节点
BinaryTreeNode node = new BinaryTreeNode<>(c);
// 如果是数字
if(Character.isDigit(c))
{
nodeStack.push(node);
}
else
if(isOperator(c))//如果是操作符
{
//从栈里弹出两个节点作为当前节点的左右子节点
BinaryTreeNode rightNode = nodeStack.pop();
BinaryTreeNode leftNode = nodeStack.pop();
node.leftChild = leftNode;
node.rightChild = rightNode;
//把当前节点入栈,作为其父节点的左右节点
nodeStack.push(node);
}
}
return nodeStack.pop();//归还二叉树根节点
}
输出结果:
二叉树根节点:*
2、二叉树还原中序表达式
至于二叉树还原中序表达式就更简单了,很容易看出对二叉树进行中序遍历即可,但是仅中序遍历时不行的,因为还没有括号。注意到二叉树的每棵子树总是要先计算的(即子树的运算优先级大于它的父节点),所以每遍历到为运算符的节点,分别加上左右括号即可保证运算优先级的正确!
Java代码:
/**
* 打印出还原的算术表达式:中序遍历到为运算符的节点,分别加上左右括号即可保证运算优先级的正确
*/
public void printMathExpression(BinaryTreeNode root)
{
if(root != null)
{
if(this.isOperator(root.data))
System.out.print("(");
printMathExpression(root.leftChild);
System.out.print(root.data);
printMathExpression(root.rightChild);
if(this.isOperator(root.data))
System.out.print(")");
}
}
看一下完整的main方法:
public static void main(String[] args)
{
InfixToPostfix16 infixToPostfix16 = new InfixToPostfix16();
Queue postfixQueue = infixToPostfix16.inToPost();
System.out.print("中缀转后缀:");
for(Character temp:postfixQueue)
System.out.print(temp);
Stack resultStack = infixToPostfix16.postfixCalculate(postfixQueue);
int value = resultStack.peek();
System.out.println("\n后缀表达式求值:"+value);
BinaryTreeNode root = infixToPostfix16.postfixToTree(postfixQueue);
System.out.println("二叉树根节点:"+root.data);
System.out.print("二叉树转算术表达式:");
infixToPostfix16.printMathExpression(root);
}
输出结果:
最后,感谢几位前辈博客:
【算法题集锦之四】--算术表达式转二叉树并还原
算术表达式的实现,支持加减乘除,括号运算,表达式转二叉树
数据结构与算法-表达式二叉树