用java解决百度之星移动火柴的问题 part 2 博客分类: Design & Architecture 百度JavajunitCC++
The next task is to evaluate a given expression. A simple way is to build a generic evaluator for the simple math expression we are dealing with. In fact, building a parser for +, -, /, * and ^(for powers) is as much work as building one for just + and -. The references we are using here are:
- http://www.smccd.net/accounts/hasson/C++2Notes/ArithmeticParsing.html
- http://www.chris-j.co.uk/parsing.php
There are tons of references on the language parsing, but these are enough for us to build a simple one. Again, the code is under 200 lines.
import java.util.List; import java.util.ArrayList; import java.util.StringTokenizer; import java.util.Stack; import java.util.Queue; import java.util.LinkedList; public class NumericExprParser { public double evaluate(String expr) { Queue<String> postFixExpr = convertToPostFixExpr(expr); return evaluatePostFixExpr(postFixExpr); } private double evaluatePostFixExpr(Queue<String> queue) { Stack<Double> stack = new Stack<Double>(); while (!queue.isEmpty()) { String t = queue.remove(); if (isNumber(t)) { stack.push(Double.parseDouble(t)); } else if (isOperator(t)) { Double n1 = stack.pop(); Double n2 = stack.pop(); // for unary operators, this is empty. if (t.equals("+")) stack.push(n2 + n1); else if (t.equals("-")) stack.push(n2 - n1); else if (t.equals("*")) stack.push(n2 * n1); else if (t.equals("/")) stack.push(n2 / n1); else if (t.equals("^")) stack.push(Math.pow(n2, n1)); } } return stack.pop(); } private Queue<String> convertToPostFixExpr(String expr) { Queue<String> queue = new LinkedList<String>(); Stack<String> stack = new Stack<String>(); String[] tokens = getTokens(expr); for (String s : tokens) { if (isNumber(s)) { queue.add(s); } else if (isOperator(s)) { while (!stack.isEmpty() && isOperator(stack.peek()) && isSameOrHigherPriority(stack.peek(), s)) { String t = stack.pop(); queue.add(t); } stack.push(s); } else if (s.equals("(")) { stack.push(s); } else if (s.equals(")")) { String t; while (!(t = stack.pop()).equals("(")) { queue.add(t); } } } while (!stack.isEmpty()) { String t = stack.pop(); queue.add(t); } return queue; } private boolean isNumber(String s) { try { Double.parseDouble(s); return true; } catch (Exception ex) { return false; } } private boolean isOperator(String s) { return s.equals("+") || s.equals("-") || s.equals("*") || s.equals("/") || s.equals("^"); } private boolean isSameOrHigherPriority(String s, String t) { if (t.equals("+") || t.equals("-")) { return true; } else if (t.equals("*") || t.equals("/")) { return s.equals("*") || s.equals("/") || s.equals("^"); } else if (t.equals("^")) { return s.equals("^"); } else return false; } private String[] getTokens(String expr) { List<String> tokens = new ArrayList<String>(); StringTokenizer st = new StringTokenizer(expr, "+-*/^()", true); while (st.hasMoreTokens()) { String s = st.nextToken().trim(); if (!s.equals("")) { tokens.add(s); } } return tokens.toArray(new String[0]); } }
This parser can handle +, -, /, *, ^ and (). Since we don't need unary -, it's not there. The parentheses is for () only, since we don't need [] and {}.
This class is sufficient for our purpose. A test case is like this:
import junit.framework.TestCase; public class NumericExprParserTest extends TestCase { private NumericExprParser exprParser = new NumericExprParser(); public void testEvaluate() { String expr = "(10 - 1) * 2 / 3 ^ 2"; double res = exprParser.evaluate(expr); System.out.println(res); assertTrue(res == 2); expr = "2 * 3 + 5 * 4 / 2^2"; res = exprParser.evaluate(expr); System.out.println(res); assertTrue(res == 11); expr = "0 -5 * 3^2"; res = exprParser.evaluate(expr); System.out.println(res); assertTrue(res == -45); } }
Note that this class is a reusable class, not just for our problem here.
With these building blocks, we are ready to attack the problem. There are several steps:
- We need to check whether the original one is already a solution, such as 1 + 1 = 2. This is because once we take a match off and it's not a number anymore, then we lose this solution. Otherwise, we just loop through all possibilities and disgard any case where we can't form numbers.
- Given a string, we convert the digits into MatchDigit AND we keep a reference from MatchDigit to the original digit so that once we have a new expression in matches, we can convert them back to strings in order to use the evaluator to evaluate.
- Once we have the MatchDigit list, we can loop through all possibilities to find the solutions. In here, I just print out the solutions. A better way would be to construct some kind of result class to return the results, but I leave that out for simplicity without much harm.
The class is as follows:
import java.util.ArrayList; import java.util.HashMap; /** * Given an arithmetic expression, by moving one match to make it a valid equation. */ public class MatchDigitEquatorSolver { private static final String DIGITS = "0123456789"; public void solve(String expr) { // check the original expression if (checkResult(expr)) { System.out.println("The original expr is a solution: expr: " + expr); } HashMap<MatchDigit, Integer> indices = new HashMap<MatchDigit, Integer>(); ArrayList<MatchDigit> mds = new ArrayList<MatchDigit>(); for (int i=0; i<expr.length(); i++) { char c = expr.charAt(i); if (isDigit(c)) { int digit = c - 48; MatchDigit md = new MatchDigit(digit); mds.add(md); indices.put(md, i); } } for (MatchDigit md : mds) // loop through all digits { for (MatchPosition mp : md.getAllMatches()) // for each digit, loop through all matches { MatchDigit newMd = md.takeMatch(mp); if (newMd == null) continue; // meaning this is not a valid digit once we remove the match. // now we have a new digit, we need to put the match somewhere for (MatchDigit md1 : mds) // loop through all digits again to see whether we can put the match somewhere. { for (MatchPosition position : MatchPosition.values()) { MatchDigit newMd1 = md1.setMatch(position); if (newMd1 == null) continue; // now we have a valid new digit, we need to check the result. // replace the old digits with the new one. String newExpr = expr.substring(0, indices.get(md)) + newMd.value() + expr.substring(indices.get(md)+1); newExpr = newExpr.substring(0, indices.get(md1)) + newMd1.value() + newExpr.substring(indices.get(md1)+1); if (checkResult(newExpr)) { System.out.println("expr: " + expr); System.out.println("new expr: " + newExpr); System.out.println("move match " + mp + " from digit[index: " + indices.get(md) + "] " + md.value() + " to the position " + position + " in the digit[index: " + indices.get(md1) + "] " + md1.value() + "."); } } } } } } private boolean isDigit(char c) { return DIGITS.indexOf(c) > -1; } private boolean checkResult(String expr) { String[] exprs = expr.split("="); NumericExprParser parser = new NumericExprParser(); double left = parser.evaluate(exprs[0]); double right = parser.evaluate(exprs[1]); return left == right; } }
Although there are still 4 for loops, they are more managable in the context as they are close to the pseudo code we present at the beginning, i.e., they are aligned with the business logic. A test case is like this:
import junit.framework.TestCase; public class MatchDigitEquatorSolverTest extends TestCase { private MatchDigitEquatorSolver solver = new MatchDigitEquatorSolver(); public void testSimple() { String a = "9 + 5 = 9"; solver.solve(a); } public void testOriginal() { String a = "4 + 5 = 9"; solver.solve(a); } public void testMoreDigits() { String a = "19 + 5 = 19"; solver.solve(a); } public void testParen() { String a = "2 * (9 + 5) = 18"; solver.solve(a); } }
Note that in our code there is no restriction to single digit number, as shown in the 3rd case in the above. And we allow ^(power) operators as well.
The results are like this:
expr: 9 + 5 = 9 new expr: 3 + 6 = 9 move match UPPERLEFT from digit[index: 0] 9 to the position LOWERLEFT in the digit[index: 4] 5. expr: 9 + 5 = 9 new expr: 3 + 5 = 8 move match UPPERLEFT from digit[index: 0] 9 to the position LOWERLEFT in the digit[index: 8] 9. The original expr is a solution: expr: 4 + 5 = 9 expr: 19 + 5 = 19 new expr: 13 + 6 = 19 move match UPPERLEFT from digit[index: 1] 9 to the position LOWERLEFT in the digit[index: 5] 5. expr: 19 + 5 = 19 new expr: 13 + 5 = 18 move match UPPERLEFT from digit[index: 1] 9 to the position LOWERLEFT in the digit[index: 10] 9. expr: 2 * (9 + 5) = 18 new expr: 2 * (3 + 6) = 18 move match UPPERLEFT from digit[index: 5] 9 to the position LOWERLEFT in the digit[index: 9] 5.
Now we are done, like we do college homework(meaning we throw them away once we get the grades). But as professional software engineers, we always wonder what's next because we need to maintain "the damn thing". What if we change our requirement, say, we can now move two matches? Or allow more operations. How flexible is our class structure to handle these changes? In the first case, we just need to write a new solver class and reuse the MatchDigit and parser classes. In the second case, we just modify the parser class or write a completely new one.
Normally, a stateless operation has less entanglement than stateful operations. For example, writing a parser for, say java, language is much harder than this example because parsing tokens heavily depends on the token series and the current position in the series, e.g., the parentheses handling, the math functions and their arguments handling.
Think, think, think ...