背景:
读大学那会上了计算机结构及算法,工作了几年,里面的内容在实际开发中很少运用,可能是自己所在的行业限制,让很多大学的知识还没有发挥的空间,感觉现在主要的就是实现业务逻辑,初中知识足够了,简单的逻辑。最近拿起书本又捡起了数据结构和算法,结合jdk的源码,感慨颇深,感觉自己这几年技术积累太少,或者说只注重了广度,深度上远远不够,这对自己以后的职业生涯是大大不利的。
行,回到正题,大学那会学了树形结构,最长用的就是二叉树。其实这几年在项目中也有运用,就是存在这种父子机构的数据就直接可以存成树形结构,这样再设计一张表存储节点信息,当然也是通过主外键来实现每条记录之间父子关系。不过树里面的先序遍历,中序遍历和后序遍历基本上就没用过。最近遇到一个问题就很有意思,就是计算四则表达式。
例如如果要用计算机来计算:(1+3)*12-3,这不是小学计算题吗?计算机来算不是小case吗?我开始也是这么想的,可真写起来代码来难度就大了,后来仔细思考下,觉得我们直觉上简单的,用计算机不一定简单,数学上能简单解决的,或许用代码来实现就没那么容易。
现在来思考计算机最容易实现什么,就是你告诉他两个数,然后再告诉它需要执行什么运算,他就可以很快算出来,而算数表达式是通过括号﹑运算优先级﹑书写顺序来进行运算的。
所以现在为了解决书写的计算式到计算机能理解的运算式的转换,需要借助二叉树结构。
探究:
基本的树的结构就这样:
考虑如果将父节点存成运算符,两个子节点存成数据,这样让计算机先读取两个子节点数据,再读取父节点运算符就可以执行运算了。
那对于复杂一点的算式呢?
如:2*3+1
先算左边叶子节点2*3得到结果6,然后6+1=7,可以得到结果。可以看到运算的数都在叶子节点上,运算符出现在非叶子节点上。而且按照上面的执行顺序刚好满足了二叉树的后序遍历,从根节点开始先访问左节点,再访问右节点,最后访问父节点,如此递归的进行。
为了简化开始的过程,我们先不考虑有括号的情况,后面再继续讨论加入括号的情况。经过规律探索可以根据算式来这样构建树:
- 先插入一个运算数,若树为空,那么这个节点作为根节点。
- 插入一个运算符*,那么这个节点作为上面的那个节点的父节点,而且上面那个节点成为它的左儿子。
- 再插入一个数字,那这个数字成为上面运算符的右儿子。
- 再插入一个运算符+,那这个运算符成为2中运算符的父节点.
- 再插入一个数字,这个数字成为2中节点的右儿子。
如果情况是这样的:
算式:2+3*1
树应该是这样:
规则变成这样:
- 先插入一个运算数,若树为空,那么这个节点作为根节点。
- 插入一个运算符+,那么这个节点作为上面的那个节点的父节点,而且上面那个节点成为它的左儿子。
- 再插入一个数字,那这个数字成为上面运算符的右儿子。
- 再插入一个运算符*,那这个运算符成为3中运算符的父节点.,3中节点成为该运算符的左儿子。
- 再插入一个数字,这个数字成为上面运算符的右儿子。
两种规则情况一整合,可以得出如下结论:
- 插入一个运算数,若树为空,那么这个节点作为根节点;若不为空,那么成为前一个运算符的右儿子。
- 插入运算符+,直接成为目前树的根节点,之前的根节点成为运算符的左子树。
- 插入运算符*,成为上一个操作数的父节点,操作数成为该运算符左儿子。
这样3条规则就符合了上述两种树的构建,之后新增操作数和符号,规则也一致,同样符号-和+规则一致,符号*和/规则一致,可以验证只要属于同一优先级的运算符规则都一致。
构建:
现在就可以开始根据规则来构建我们的二叉树了,先定节点类,如下:
class Node{
public String text;
public Node(String text){
this.text=text;
}
public Node leftNode,rightNode;
}
再定义树类:
class Tree{
public Node root;
public Tree(Node root){
this.root=root;
}
public Tree(){}
//判断一个字符串是否是数字
private static boolean isNum(String s){
char[] a=s.toCharArray();
for(int i=0;i<a.length;i++){
if(a[i]<'0'||a[i]>'9'){
return false;
}
}
return true;
}
//加入元素方法,可以加入数字字符串,符号字符串,ch是加入的字符串变量,preNode是上一个节点
public Node addElement(String ch,Node preNode){
Node node=new Node(ch);
if(isNum(ch)){
if(root==null){
this.root=node;
}else{
preNode.rightNode=node;
}
}else if(ch.equals("+")||ch.equals("-")){
node.leftNode=this.root;
this.root=node;
}else if(ch.equals("*")||ch.equals("/")){
//这里是成为上一个节点的父节点 的操作,由于子节点还没有提供方法指向父节点,所以这里采用了调换节点中存储的text方式来实现
node.text=preNode.text;
preNode.text=ch;
preNode.leftNode=node;
return preNode;
}
return node;
}
//递归的方式进行计算,和树的后序遍历方法一致
public int count(Node node){
if("+".equals(node.text)){
return count(node.leftNode)+count(node.rightNode);
}else if("-".equals(node.text)){
return count(node.leftNode)-count(node.rightNode);
}else if("*".equals(node.text)){
return count(node.leftNode)*count(node.rightNode);
}else if("/".equals(node.text)){
return count(node.leftNode)/count(node.rightNode);
}else{
return Integer.parseInt(node.text);
}
}
//静态方法,读取算式的文本信息,逐个元素加入到树中,最后根据构建好的树进行四则运算
public static int countZ(String exp){
Tree tree=new Tree();
Node pre=null;
int i=1;
StringBuilder numS=new StringBuilder();
//flag用于标记截取的字符串是否是数字
int flag=0;
while(i<=exp.length()){
String s=exp.substring(i-1,i);
if(isNum(s)){
//判断截取字符是数字需要进一步判断下一个字符
numS.append(s);
flag=1;
if(i==exp.length()){
tree.addElement(numS.toString(),pre);
}
}else{
//上一个字符是数字,此字符是运算符
if(flag==1){
Node node=tree.addElement(numS.toString(),pre);
Node nodeC=tree.addElement(s,node);
pre=nodeC;
flag=0;
numS.delete(0,numS.length());
}else{
//上一个字符不是数字,直接加入树中
Node nodeC=tree.addElement(s,pre);
pre=nodeC;
}
}
i++;
}
return tree.count(tree.root);
}
}
调用上面代码计算不带括号的运算式:
public static void main(String[] args) {
/*
Scanner in=new Scanner(System.in);
while(in.hasNext()){
String exp=in.nextLine();
exp=ToExp(exp);
System.out.println(Count(exp));
}
*/
String exp="3+2*4";
int result=Count(exp);
System.out.println(result);
}
控制台输出结果:11,正确。
更进一步:
如果加入括号运算,结果会怎样呢?
思考,加入括号相当于将括号内的分割成了子算式,算出子算式就可以去括号了,例如:
(1+2)3+2
这个可以先算1+2=3,将3带入,且可以去掉括号得:
33+2,又转换为了无括号的运算。
从上面例子可以看出,我们可以一层一层去掉括号,然后把括号内的运算结果直接体现在括号里,并且去掉括号,这样就可以了。如果有多层括号那就依次方法,一层一层去,一直到没有括号的运算位置。看到没,刚好是个递归。代码实现如下:
public static int Count(String exp){
//括号起点
int start=exp.indexOf("(");
//与上面"("对应的")"位置
int end=GetEnd(exp);
//没有括号后跳出
if(start<0){
return Tree.countZ(exp);
}
//截取到子字符串表达式
String subExp=exp.substring(start+1,end);
//将子字符串表达式运算结果替换原来的子表达式,且去括号
String newExp=exp.substring(0,start)+Count(subExp)+exp.substring(end+1);
return Count(newExp);
}
这里需要注意的是GetEnd()函数,怎么判定一个”)“与第一个出现的”(“对应呢?需要遵从下面规则:
- 在第一个”(“之后
- 在第一次出现了”)“之后
- 在2出现之后的”(“之前
- 在2和3之间的最后一个”)“
举个例子:
((1+2)+3)+(3+4)
判定第一个”(“对应的”)“,用上述规则就可以找到,现实现代码如下:
public static int GetEnd(String exp){
int i=0;
int j=exp.length();
while(i<exp.length()-1){
//出现第一个")"
if(exp.substring(i,i+1).equals(")")){
j=i;
}
if(i>j){
//再出现”)“之后第一次出现”(“
if(exp.substring(i,i+1).equals("(")){
break;
}
}
i++;
}
i++;
//截取字符串,找到此时最后一个”)“
int end=exp.substring(0,i).lastIndexOf(")");
return end;
}
OK搞定,现在随意输入个带小括号的表达式看看:
public static void main(String[] args) {
/*
Scanner in=new Scanner(System.in);
while(in.hasNext()){
String exp=in.nextLine();
exp=ToExp(exp);
System.out.println(Count(exp));
}
*/
String exp="((3+2)*4+12)-(1+5)";
int result=Count(exp);
System.out.println(result);
}
控制台显示:
26
正确。
现在还有个小问题,如果出现了中括号,大括号则呢么办?简单,直接替换为小括号就行,括号起的作用都是一样的。
public static String ToExp(String exp){
return exp.replace("[", "(").replace("{", "(").replace("]", ")").replace("}",")");
}
结束语:
以上是通过二叉树来实现四则运算的整个过程,当然还存在许多不足,比如还需要判定一个表达式是否合法,比如除法运算如果除不尽该如何处理,怎么计算带有小数的,怎么插入更多的运算符等。这个还需要进一步探索,上面方法有不对的地方还请各位技术达人指正。
下一篇博客想实现自动生成给定复杂程度的四则运算,然后让计算机来运算,这样做的目的是直接出一些题目来给小朋友算,然后电脑来给定答案。进一步可能结合图像识别算法,可以拍照四则运算表达式,然后计算批改作业。