随笔 - 18  文章 - 96  trackbacks - 0
<2011年11月>
303112345
6789101112
13141516171819
20212223242526
27282930123
45678910


常用链接

留言簿(4)

随笔档案

相册

我的兄弟们

搜索

  •  

最新评论

阅读排行榜

评论排行榜

很久没有回来这里写技术BLOG了,这里的氛围还行,大家都对一个问题积极的思考(至少之前这里给我的感觉是这样的),2年里面自己也忙着做些事情,没有写,最近有空也就写写,偶尔会去oschine.net看看新闻,然后就在那里看到了一个人提出的问题很有意思,就是怎么表达式求解,例如(1 + 2) / 3 - 1 * 2 + 5 / (3 + 2)这样的字符串输入,怎么样解析之后输出结果。说来也好笑,对于我这个不是计算机专业出身的人,自然也不知道用逆波兰和递归下降(后来看回帖才知道有这两个算法,然后google之发现全是这样的算法求解),我的目标很简单,就是想去解决这个问题。所以我花了近3小时(太笨)从构思到写范例程序到调试了一番,结果还不错,所以记录下来,给看多了逆波兰,看多了标准递归下降的人一点新鲜空气。

其实我的想法很简单,就是模拟人的思维去解决这个问题,我们人解决算式的时候,不管算得多快,都是a+b,a-b,a*b,a/b这样最原始的一个一个的求解的,所以我把这样的方式称为原子表达式,就是不可再分割的表达式,而我的解法就是要去将表达式分解成原子的之后进行计算,然后再递归回来,最后得出答案。很朴素,很简单的想法吧。就拿(1 + 2) / 3 - 1 * 2 + 5 / (3 + 2)这个来分析过程吧。我们先看到(),所以取出()中的内容,刚好,()中的内容是原子的,1+2,于是我们得出答案3,然后与后面的式子形成新的表达式,3/3-1*2+5/(3+2),这时候我们继续找(),3+2,之后的表达式成了3/3-1*2+5/5,然后继续找括号,没有了,于是我们按照计算法则,先乘除,找到*/,然后取出原子式3/3,于是有了1-1×2+5/5,然后继续,1×2,1-2+5/5,然后继续,5/5,有了1-2+1,然后没有乘除了,我们来算加减,找到1-2,于是有了-1+1,找到-1+1,最后输出0。

下面贴出我的范例代码,没有重构,没有优化(比如可以尝试将平级的(),*/和+,-一次迭代就计算完毕,上例中的(1+2)和(3+2)一次迭代计算完成3/3-1×2+5/5,然后3/3,1×2,5/5一次迭代完成1-2+1,然后再完成1-2,-1+1,这样便少了几步表达式的重新组合),测试的算式也就几个,仅仅是一个示意,如有BUG,可自行修改。还是老规矩,先上运算结果吧。
1+(-1-2)/3-2*(-2+1) = 2.0
122+2*(11-1)/(-3-(2-3)) = 112.0
-2*3 + (-3+4) = -5.0
((1 + 2) / 3 + 1) * 2 = 4.0
(1 + 2) / 3 - 1 * 2 + 5 / (3 + 2) = 0.0
(1.5 + 2.5) / 4 + 1 * 2 - 5 / (2 - 4) = 5.5
10-4+123+100*98*8/100*4*3+3-12/3/4*2*12*12/4+4-8+12/3*12+12 = 9524.0

  1 public class Cal {
  2 
  3     public static void main(String[] args) {
  4         Cal cal = new Cal();
  5         String expression = "1+(-1-2)/3-2*(-2+1)";
  6         double result = cal.caculate(expression);
  7         System.out.println(expression + " = " + result);
  8         expression = "122+2*(11-1)/(-3-(2-3))";
  9         result = cal.caculate(expression);
 10         System.out.println(expression + " = " + result);
 11         expression = "-2*3 + (-3+4)";
 12         result = cal.caculate(expression);
 13         System.out.println(expression + " = " + result);
 14         expression = "((1 + 2) / 3 + 1) * 2";
 15         result = cal.caculate(expression);
 16         System.out.println(expression + " = " + result);
 17         expression = "(1 + 2) / 3 - 1 * 2 + 5 / (3 + 2)";
 18         result = cal.caculate(expression);
 19         System.out.println(expression + " = " + result);
 20         expression = "(1.5 + 2.5) / 4 + 1 * 2 - 5 / (2 - 4)";
 21         result = cal.caculate(expression);
 22         System.out.println(expression + " = " + result);
 23         expression = "10-4+123+100*98*8/100*4*3+3-12/3/4*2*12*12/4+4-8+12/3*12+12";
 24         result = cal.caculate(expression);
 25         System.out.println(expression + " = " + result);
 26     }
 27 
 28     double caculate(String expression) {
 29         expression = clean(expression);
 30         return parse(expression);
 31     }
 32 
 33     private String clean(String expression) {
 34         StringBuilder sb = new StringBuilder();
 35         char[] chars = expression.toCharArray();
 36         for (char c : chars) {
 37             if (c != ' ')
 38                 sb.append(c);
 39         }
 40         return sb.toString();
 41     }
 42 
 43     private double parse(String expression) {
 44 //        System.out.println(expression);
 45         char[] chars = expression.toCharArray();
 46         // 已经是原子状态,则直接计算
 47         if (isAtomic(expression)) {
 48             int delimIndex = findFirstDelimIndex(expression);
 49             char delim = expression.charAt(delimIndex);
 50             double first = Double.valueOf(expression.substring(0, delimIndex));
 51             double second = Double.valueOf(expression.substring(delimIndex + 1));
 52             switch (delim) {
 53             case '+':
 54                 return first + second;
 55             case '-':
 56                 return first - second;
 57             case '*':
 58                 return first * second;
 59             case '/':
 60                 return first / second;
 61             default:
 62                 throw new RuntimeException("parse error. 未知的运算符号");
 63             }
 64         }
 65         // 不是原子状态,那就来解析表达式
 66         // 先查找是否有()的状态
 67         int first = -1;
 68         int last = -1;
 69         for (int i = 0; i < chars.length; i++) {
 70             char c = chars[i];
 71             if (c == '(') {
 72                 first = i;
 73             } else if (c == ')') {
 74                 last = i;
 75                 break// 如果遇到)括号了,则说明有包含关系,退出,进行解析。
 76             }
 77         }
 78         if ((first >= 0 && first >= last)) {
 79             throw new RuntimeException("parse error.表达式错误,缺少反括号");
 80         }
 81         if (first >= 0 && last > first) { // 正常情况
 82             String newExpression = expression.substring(first + 1, last);
 83             // 将括号中的表达式弄出来,进行计算
 84             double result = parse(newExpression);
 85             // 然后将计算结果替换这个表达式,然后递归处理
 86             StringBuilder sb = new StringBuilder();
 87             if (first > 0)
 88                 sb.append(expression.substring(0, first));
 89             sb.append(result);
 90             sb.append(expression.substring(last + 1));
 91             return parse(sb.toString());
 92         }
 93 
 94         // 不是括号,那么就来处理乘除,3+3*2/3*2 + 1这样的情况
 95         last = -1;
 96         for (int i = 0; i < chars.length; i++) {
 97             char c = chars[i];
 98             if (c == '*' || c == '/') {
 99                 last = i;
100                 break// 如果遇到*或者/了,则先进行解析。
101             }
102         }
103         if (last >= 0) {
104             // 找到之后,就看靠近的前面是否有+,-运算符,如果有则,得到那个+,-的index(也有可能是负数),然后便可通过index得到*,/前面的数字
105             // 然后后面是否有+,-,*,/运算符,如果存在,则得到index,然后通last和Index来得到后一个数字
106             // 然后形成新的expression,进行计算,然后通过计算结果替换这个expression,继续解析
107             // 如 2+3+3*2/3*2 +1 ,在完成之后形成新的2+3+6/3*2+1,继续进行解析
108             String forward = expression.substring(0, last);
109             String appendForward = null;
110             int addIndex = forward.lastIndexOf("+");
111             int divIndex = forward.lastIndexOf("-"); // 减号要特殊处理,有可能这是个负数
112             if (divIndex > 0 && isDelim(forward.charAt(divIndex - 1))) {
113                 divIndex = divIndex - 1;
114             }
115             StringBuilder sb = new StringBuilder();
116             if (addIndex != -1 && addIndex >= divIndex) { // 这里>=是因为可能是因为减号其实是负号,所以移位了
117                 sb.append(forward.substring(addIndex + 1));
118                 appendForward = forward.substring(0, addIndex + 1);
119             } else if (divIndex != -1 && addIndex < divIndex) {
120                 sb.append(forward.substring(divIndex + 1));
121                 appendForward = forward.substring(0, divIndex + 1);
122             } else {
123                 sb.append(forward); // 前面没有符号,直接就是数字(有可能是负数)
124             }
125 
126             sb.append(expression.charAt(last)); // 运算符号
127 
128             String backward = expression.substring(last + 1);
129             String appendBackward = null;
130             int index = findFirstDelimIndex(backward);
131             if (index != -1) {
132                 sb.append(backward.substring(0, index));
133                 appendBackward = backward.substring(index);// 这里自带了运算符,后面不用加了
134             } else {
135                 sb.append(backward);
136             }
137             double result = parse(sb.toString());
138 
139             sb = new StringBuilder();
140             if (appendForward != null)
141                 sb.append(appendForward);
142             sb.append(result);
143             if (appendBackward != null)
144                 sb.append(appendBackward);
145             return parse(sb.toString());
146         }
147 
148         // 不是括号,也不是乘除,只有加减,例如 3+2+1-5-3
149         last = -1;
150         for (int i = 0; i < chars.length; i++) {
151             char c = chars[i];
152             if (c == '+' || (c == '-' && i != 0)) {
153                 last = i;
154                 break// 谁是第一个,就弄谁
155             }
156         }
157         if (last >= 0) {
158             // 查找后面是否有+,-,*,/运算符,如果存在,则得到index,然后通last和Index来得到后一个数字
159             // 然后形成新的expression,进行计算,然后通过计算结果替换这个expression,继续解析
160             // 如 2+3+3+1 ,在完成之后形成新的5+3+1,继续进行解析
161             String forward = expression.substring(0, last);
162             StringBuilder sb = new StringBuilder();
163             sb.append(forward); // 前面没有符号,直接就是数字(有可能是负数)
164             sb.append(expression.charAt(last)); // 运算符号
165             String backward = expression.substring(last + 1);
166             String appendBackward = null;
167             int index = findFirstDelimIndex(backward);
168             if (index != -1) {
169                 sb.append(backward.substring(0, index));
170                 appendBackward = backward.substring(index); // 这里自带了运算符,后面不用加了
171             } else {
172                 sb.append(backward);
173             }
174             double result = parse(sb.toString());
175             sb = new StringBuilder();
176             sb.append(result);
177             if (appendBackward != null) {
178                 sb.append(appendBackward);
179             }
180             return parse(sb.toString());
181         }
182         return 0;
183     }
184 
185     private boolean isDelim(char c) {
186         return (c == '+' ||  c == '*' || c == '/' || c == '-');
187     }
188 
189     /**
190      * 查找第一个运算符号的index
191      */
192     private int findFirstDelimIndex(String expression) {
193         char[] chars = expression.toCharArray();
194         for (int i = 0; i < chars.length; i++) {
195             char c = chars[i];
196             if (c == '+' || c == '*' || c == '/') {
197                 return  i;
198             }
199             if (c == '-') {
200                 if (i != 0 && chars[i - 1!= '+' && chars[i - 1!= '-' && chars[i - 1!= '*' && chars[i - 1!= '/'// 前面有可能是负数
201                     return i;
202             }
203         }
204         return -1;
205     }
206 
207     /**
208      * 是否是原子式子
209      */
210     private boolean isAtomic(String expression) {
211         char[] chars = expression.toCharArray();
212         int delimCount = 0;
213         for (int i = 0; i < chars.length; i++) {
214             char c = chars[i];
215             if (c == '+' ||  c == '*' || c == '/' || (c == '-' && i != 0 && chars[i - 1!= '+' && chars[i - 1!= '-' && chars[i - 1!= '*' && chars[i - 1!= '/'))
216                 delimCount++;
217         }
218         return delimCount == 1;
219     }
220 }

请忽略我代码的英文,我随便写的,这里如果取消注释掉的44行,我们就可以看到运算轨迹了,我帖2个比较简单的,1个是我们刚刚提到的范例,一个是有双括号的,看看算法是否按我心意,快快显灵。
第一个:(1 + 2) / 3 - 1 * 2 + 5 / (3 + 2),可以看到先清空了空格,方便取得下标。
(1+2)/3-1*2+5/(3+2)
1+2
3.0/3-1*2+5/(3+2)
3+2
3.0/3-1*2+5/5.0
3.0/3
1.0-1*2+5/5.0
1*2
1.0-2.0+5/5.0
5/5.0
1.0-2.0+1.0
1.0-2.0
-1.0+1.0
(1 + 2) / 3 - 1 * 2 + 5 / (3 + 2) = 0.0
嗯,跟算法的运算轨迹是一致的。
第二个:122+2*(11-1)/(-3-(2-3)),双括号嵌套的。
122+2*(11-1)/(-3-(2-3))
11-1
122+2*10.0/(-3-(2-3))
2-3
122+2*10.0/(-3--1.0)
-3--1.0
122+2*10.0/-2.0
2*10.0
122+20.0/-2.0
20.0/-2.0
122+-10.0
122+2*(11-1)/(-3-(2-3)) = 112.0
嗯,不错,不错,跟算法所设想的运算轨迹是一致的。

本文的目的在于能通过这样一个老问题构思出的不同的解法来抛砖引玉,希望如果要回帖的童鞋们不要简单的说,用中缀转后缀(逆波兰)多简单啊,用递归下降(这个知道的可能不如逆波兰多)多简单啊,这样我就是抛砖引瓦了,这种教科书式的方式在已知问题和解答的情况下咱们可以照本宣科,但是假设我们遇到一个全新的问题,而且没有教科书告诉你用这个方法去解的情况,就是逼自己想办法的时候了。

PS1:
有回帖说,这也算递归下降,并且说我的原子式是树叶子节点,也就是说这位仁兄把我的处理过程映射到树的结构中去,我觉得树的这个想法很好,所以我立刻尝试了并构造了一个树结构(1 + 2) / 3 - 1 * 2 + 5 / (3 + 2),这里假设我们的节点有{运算符,数字,结果}三个字段。
                                               +
                                          /           \
                                        -              /
                                     /       \        /    \
                                    /         *     5     +
                                /     \      /   \        /   \
                               +      3    1    2     3     2
                              /   \
                            1     2
这样,这个树的特点就是最底层的子节点都是数字,如果该节点存在子节点(左,右,且不是数字),那么继续向下,如果是数字则返回,然后看右边的子节点是否是数字,如果不是继续向下,直到左右子节点都是数字,然后得到父节点的运算符,然后进行计算,并且将结果存储起来,然后递归回去,最后得到结果。我来模拟一下这个树的变化,第一次,我们找到了1+2,所以
计算之后变为了:      
                                              +
                                          /           \
                                        -              /
                                     /       \        /    \
                                    /         *     5     +
                                /     \      /   \        /   \
                               3      3    1    2     3     2
这个时候回到“/”这个节点,然后查看右节点,是数字,左右子节点都是数字了计算出结果之后:
                                              +
                                          /           \
                                        -              /
                                     /       \        /    \
                                    1         *     5     +
                                             /   \        /   \
                                             1    2     3     2
这个时候回到“-”号,但是右边还是“×”号,所以继续查看“×”号的左右子节点,是数字,计算结果:
                                              +
                                          /           \
                                        -              /
                                     /       \        /    \
                                    1         2     5     +
                                                           /   \
                                                         3     2
接着×号有了结果,回到“-”号,左右都是数字了,计算结果,以此类推,最后得到结果为0。当然这是我根据那位仁兄树结构的提示构思的运算过程,那么运算过程有了,如何通过表达式构造树,还有就是我这个树的构造方式是否正确,我就留给大家了,重要的是能引起大家的思考,一个问题带来的不同解法(直吸管变成弯吸管就算创新了,所以大家要是有一点点的想法或就在我这个方法上有改进也可以说出来探讨),再次感谢那位回帖的仁兄。
posted on 2011-11-09 10:36 ruislan 阅读(1743) 评论(6)  编辑  收藏

FeedBack:
# re: 表达式求解的思考 2011-11-09 17:15 医疗保险
偶看把  回复  更多评论
  
# re: 表达式求解的思考 2011-11-09 20:05 Doyle
请楼主自行考虑下,从原表达式到达原子表达式的过程是否是一个递归下降的过程?你这个解析,一样从原表达式一层层的拆解为一个计算数,树的叶子节点就是你的原子表达式,然后再一层层返回上来的。

这还真不算什么新鲜。  回复  更多评论
  
# re: 表达式求解的思考 2011-11-09 20:16 博洋家纺
?你这个解析,一样从原表达式一层层的拆解为一个计算数  回复  更多评论
  
# re: 表达式求解的思考 2011-11-10 08:08 tb
不错 学习了   回复  更多评论
  
# re: 表达式求解的思考 2011-11-10 14:41 tb
很好,学习了  回复  更多评论
  
# re: 表达式求解的思考[未登录] 2011-11-15 20:46 m
表达式计算?
看到你这么多内容就头疼,还不如去研究波兰表达式呢
  回复  更多评论
  

只有注册用户登录后才能发表评论。


网站导航: