转自我的个人博客,原文地址http://mistariano.com/blog/12
只是我个人的一些实现思路,抛砖引玉~
多项式类 PolyExp/PolyTerm
设计PolyExp
和PolyTerm
两个类对多项式抽象。一个多项式是若干项的加和,因此很容易想到多项式对象has-a一组项。
考虑到同类项合并的天然性质,用Map
维护多项式的项,进一步考虑数据范围约束(有效输入不长于1000),选用HashMap
PolyTerm
实现一个add
方法用于合并同类项,PolyExp
也实现一个add
方法,实现Exp与Exp、Exp与Term间的加法。一个新项加入多项式后立即检查并合并同类项
重载PolyTerm
的toString
,可以返回该项的最短合法形式(若为0则返回空串)
重载PolyExp
的toString
,遍历所有Term并将其toString
加和,注意这里如果加和是空串,说明所有Term的语义都是“0”,因此要特判并返回"0"
;对于第一个字符是'+'
的情况,扔掉加号来优化字符串长度
这里有一个小trick,为了尽量让首位是正号,在遍历Term前先依照其系数降序对其排序。因此实现了PolyTerm
类的compareTo
方法。排序还有一个好处是,如果排序结果是确定的(这意味着我们需要先排系数,系数相等时还要排指数),可以直接根据两个PolyExp
的toString
返回值判断两个PolyExp
是否相等,便于设计单元测试。因此还额外实现了两个类的equalTo
求导的实现其实是很简单的,毕竟只需要符号求导。Exp求导方法的返回值也是Exp对象,这样可以复用Exp类的各种方法,合情合理。
其实多项式类和求导不是这次的难点,难点是处理输入。
处理魔鬼输入:PolyPattern 和 PolyMultiMatcher
拆解一下输入的需求:
我们有如下类型的语素:
- 加号+或减号-
- 乘号*
- 上标^
- 符号整数
[+-]?[0-9]+
一个字符串是合法输入的必要条件:它是任意数量空格和/或制表符连接若干语素得到的串
进一步地,我们继续分割得到如下几个类型的语素:
- 连接多项式项的加号+或减号-
- 代表系数1或-1的加号+或减号-
- 乘号*
- 上标^
- 代表系数的符号整数
- 代表指数的符号整数
我们分别用
- S 表示 一个连接多项式项的加号+或减号-(
[+-]
) - + 表示 代表系数1或-1的加号+或减号-,允许缺失(
[+-]?
) - * 表示 乘号*(
\*
) - ^ 表示 上标^(
^
) - c 表示 代表系数的符号整数(
[+-]?[0-9]+
) - i 表示 代表指数的符号整数(
[+-]?[0-9]+
)
那么一个合法的包含前面加号或减号的表达式项,应当是如下七种情况之一:
- Sc
- Sx
- Sc*x
- S+x
- Sx^i
- Sc*x^i
- S+x^i
PolyPattern
是这样一个类:我输入一个形如c*x^i
的字符串,它能自动将其转换为对应的正则表达式,并可以根据getCoef
和getIndex
两个方法提取c
和i
对应的group。这样搞我个人觉得有两个好处:
- 更可读
- 更易扩展。比如某天要写个对数函数加和求导
PolyMultiMathcer
则是同时管理多个PolyPattern
,对于一个给定的字符串,每次用所有Pattern尝试提取其最前面的一个合法表达式项,从所有提取结果中,贪心地选择最长的结果作为当前的匹配结果。注意由于只需提取字符串开头的子串,因此进一步用^
锚点优化PolyPattern
生成的正则表达式
迭代这个过程,若一个字符串不能完成匹配,则其必定不是合法输入,反之则可认为提取成功。这个提取逻辑封装为PolyParser
类,单例模式。
总结
这次作业是一次很好的练习,一方面需要最基本的OO抽象能力,另一方面也让我对正则有了更深的理解
设计上的弯路还是走了很多的,比如在实现目前的PolyPattern
前试过一个大正则表达式和一个状态机,这三种设计的优劣在我上一篇博客里简要分析了
以及PolyTerm
和PolyExp
都实现了一个可导接口,这个设计其实是不必要的。大概是看见自动求导就想起刚读完的Keras源码了吧23333 深受毒害
期待接下来的Tasks。
附上代码
package homework.poly.parse.matcher;
import homework.poly.PolyExp;
import jdk.nashorn.internal.runtime.regexp.joni.exception.ValueException;
import java.math.BigInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class PolyPattern {
private static String BIG_INT_PATTERN = "([-+]?[0-9]+)";
private static String PM_PATTERN = "([-+])";
private static String COEF_PM_PATTERN = "([-+])?";
private static String BLANK_PATTERN = "[\\ \\t]*";
private final int signGroupInd = 1;
private String rePatternStr;
private String polyPatternStr;
private Pattern rePattern;
private Matcher matcher;
private int coefGroupInd = 0;
private int indexGroupInd = 0;
private BigInteger defaultIndex = BigInteger.ONE;
private BigInteger defaultCoef = BigInteger.ONE;
private PolyPattern(String polyPatternStr, boolean setDefaultIndexToZero) {
this.polyPatternStr = polyPatternStr;
rePatternStr = buildRePatternStr();
rePattern = Pattern.compile(rePatternStr);
if (setDefaultIndexToZero) {
defaultIndex = BigInteger.ZERO;
}
}
/**
* compile a matcher by a polyPatternStr String.
* <p>
* reg exp can be compiled automatically
*
* @param polyPatternStr a polyPatternStr string.
* a polyPatternStr should be a string contains
* only [ c | + | * | x | ^ | i ]
* <p>
* where
* <p>
* - c is a number placeholder, for coef
* <p>
* - + is a sign placeholder, for +/- before x
* (e.g. -x, + x, - x^2)
* <p>
* - i is a number placeholder, for index
* @param setDefaultIndexToZero set the default index value from 1 to 0
*/
public static PolyPattern compile(String polyPatternStr,
boolean setDefaultIndexToZero) {
String striped = polyPatternStr.replaceAll("\\s", "");
Pattern checkPattern = Pattern.compile("^[c+*x^i]+$");
if (!checkPattern.matcher(striped).find()) {
throw new ValueException("Bad poly pattern str: " + polyPatternStr);
}
return new PolyPattern(polyPatternStr, setDefaultIndexToZero);
}
public String getPolyPatternStr() {
return polyPatternStr;
}
private String buildRePatternStr() {
StringBuilder builder = new StringBuilder();
builder.append("^");
builder.append(BLANK_PATTERN);
builder.append(PM_PATTERN);
builder.append(BLANK_PATTERN);
int groupCount = 2;
for (char c :
getPolyPatternStr().toCharArray()) {
switch (c) {
case 'c': {
builder.append(BIG_INT_PATTERN);
coefGroupInd = groupCount;
groupCount += 1;
break;
}
case 'i': {
builder.append(BIG_INT_PATTERN);
indexGroupInd = groupCount;
groupCount += 1;
break;
}
case '+': {
builder.append(COEF_PM_PATTERN);
coefGroupInd = groupCount;
groupCount += 1;
break;
}
case 'x': {
builder.append("x");
break;
}
case '^': {
builder.append("\\^");
break;
}
case '*': {
builder.append("\\*");
break;
}
default: {
throw new ValueException("Unexpected char \'" + c + "\'");
}
}
builder.append(BLANK_PATTERN);
}
return builder.toString();
}
Matcher matcher(String str) {
matcher = rePattern.matcher(str);
return matcher;
}
boolean find() {
return matcher.find();
}
private BigInteger getSign() {
if (matcher.group(signGroupInd) == null) {
return BigInteger.ONE;
}
if (matcher.group(signGroupInd).equals("-")) {
return BigInteger.ONE.negate();
} else {
return BigInteger.ONE;
}
}
private BigInteger getCoef() {
if (coefGroupInd == 0) {
return defaultCoef;
}
String coefStr = matcher.group(coefGroupInd);
if (coefStr == null) {
return BigInteger.ONE;
} else if (coefStr.equals("-")) {
return BigInteger.ONE.negate();
} else if (coefStr.equals("+")) {
return BigInteger.ONE;
}
return new BigInteger(coefStr);
}
private BigInteger getIndex() {
if (indexGroupInd == 0) {
return defaultIndex;
}
return new BigInteger(matcher.group(indexGroupInd));
}
PolyExp getTermExp() {
return new PolyExp(getIndex(), getCoef().multiply(getSign()));
}
String getMatchedString() {
return matcher.group(0);
}
@Override
public String toString() {
return "PolyPattern{" +
"\'" + polyPatternStr + '\'' +
'}';
}
}
package homework.poly.parse.matcher;
import homework.poly.PolyExp;
import java.util.LinkedList;
public class PolyMultiMatcher {
private LinkedList<PolyPattern> patterns = new LinkedList<>();
private PolyPattern cachedPattern;
private PolyMultiMatcher() {
addMatcher("c*x^i");
addMatcher("+x^i");
addMatcher("x^i");
addMatcher("c*x");
addMatcher("+x");
addMatcher("x");
addMatcher("c", true);
}
public static PolyMultiMatcher getInstance() {
return PolyMatcherMgrSingleton.singleton;
}
private void addMatcher(String polyPattern, boolean isConstant) {
PolyPattern matcher = PolyPattern.compile(polyPattern, isConstant);
patterns.add(matcher);
}
private void addMatcher(String polyPattern) {
addMatcher(polyPattern, false);
}
public void matchAll(String str, boolean debug) {
for (PolyPattern pattern :
patterns) {
pattern.matcher(str);
}
if (debug) {
System.out.println("===========================");
System.out.println("Matching " + str);
}
}
public boolean findAny(boolean debug) {
boolean res = false;
PolyPattern currentBestPtn = null;
int maxLen = 0;
for (PolyPattern matcher :
patterns) {
boolean find = matcher.find();
if (debug) {
System.out.println("matcher = " + matcher);
System.out.println("find = " + find);
}
if (find) {
res = true;
if (matcher.getMatchedString().length() > maxLen) {
currentBestPtn = matcher;
maxLen = matcher.getMatchedString().length();
}
}
}
cachedPattern = currentBestPtn;
if (debug) {
System.out.println("cachedPattern = " + cachedPattern);
}
return res;
}
public PolyExp getTermExp() {
return cachedPattern.getTermExp();
}
public int getMatchedLength() {
return cachedPattern.getMatchedString().length();
}
private static class PolyMatcherMgrSingleton {
private static final PolyMultiMatcher singleton =
new PolyMultiMatcher();
}
}
package homework.poly.parse;
import homework.poly.PolyExp;
import homework.poly.parse.matcher.PolyMultiMatcher;
import java.util.regex.Pattern;
public class PolyParser {
/**
* Default constructor
*/
private PolyParser() {
}
public static PolyParser getInstance() {
return PolyParserSingleton.singleton;
}
/**
* Parse a PolyExp from giving string
*
* @param input just a string
* @return parsed expression. if failed, return null
*/
public PolyExp parseExp(String input) {
return parseExp(input, false);
}
public PolyExp parseExp(String input, boolean debug) {
String buffer = preprocessInput(input);
PolyMultiMatcher matcher = PolyMultiMatcher.getInstance();
PolyExp resExp = new PolyExp();
while (!buffer.isEmpty()) {
matcher.matchAll(buffer, debug);
if (matcher.findAny(debug)) {
resExp.add(matcher.getTermExp());
buffer = buffer.substring(matcher.getMatchedLength());
} else {
return null;
}
}
return resExp;
}
/**
* Check & add a '+' at the head of giving string.
*
* @param input just input
* @return a string whose first visible character is '+' or '-'
*/
private String preprocessInput(String input) {
Pattern headSignPattern = Pattern.compile("^\\s*[-+]");
if (!headSignPattern.matcher(input).find()) {
return "+" + input;
} else {
return input;
}
}
/**
* Singleton keeper class
*/
private static class PolyParserSingleton {
private static final PolyParser singleton = new PolyParser();
}
}
package homework.poly;
import java.math.BigInteger;
import java.util.Objects;
/**
* Poly Term.
* <p>
* Contains an index and a coefficient
*/
public class PolyTerm implements Derivable {
private final BigInteger index;
private BigInteger coef;
PolyTerm(BigInteger ind, BigInteger coe) {
index = ind;
coef = coe;
}
PolyTerm(int ind, int coe) {
this(new BigInteger(String.valueOf(ind)),
new BigInteger(String.valueOf(coe)));
}
static PolyTerm constant(BigInteger coe) {
return new PolyTerm(BigInteger.ZERO, coe);
}
BigInteger getIndex() {
return index;
}
private BigInteger getCoef() {
return coef;
}
PolyTerm add(PolyTerm other) {
assert (other.getIndex().equals(index));
coef = coef.add(other.getCoef());
return this;
}
void negate() {
coef = coef.negate();
}
@Override
public PolyTerm getDerivative() {
if (index.equals(BigInteger.ZERO)) {
return new PolyTerm(BigInteger.ZERO, BigInteger.ZERO);
} else {
return new PolyTerm(
index.subtract(BigInteger.ONE), index.multiply(coef));
}
}
/**
* format as coef*x^index
* <p>
* return "" if coef = 0
* <p>
* return x^index if coef = 1
* <p>
* return -x^index if coef = -1
* <p>
* return coef as a constant if index = 0
* <p>
* return coef*x if index = 1
*
* @return formatted string
*/
@Override
public String toString() {
// zero
if (BigInteger.ZERO.equals(coef)) {
return "";
}
String coefStr = coef.toString();
if (coef.compareTo(BigInteger.ZERO) > 0) {
coefStr = "+" + coefStr;
}
if (BigInteger.ZERO.equals(index)) {
// constant
return coefStr;
} else {
coefStr = coefStr + "*";
if (coef.equals(BigInteger.ONE)) {
coefStr = "+";
} else if (coef.equals(BigInteger.ONE.negate())) {
coefStr = "-";
}
if (BigInteger.ONE.equals(index)) {
return coefStr + "x";
} else {
return coefStr + "x" + "^" + index.toString();
}
}
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
PolyTerm polyTerm = (PolyTerm) o;
return Objects.equals(index, polyTerm.index) &&
Objects.equals(coef, polyTerm.coef);
}
/**
* greater coef first
* <p>
* if coef equals, less index first
*
* @param other the other one
* @return -1 if less than, 1 if greater than, 0 if equal
*/
int compareTo(PolyTerm other) {
if (coef.compareTo(other.getCoef()) != 0) {
return -coef.compareTo(other.getCoef());
} else {
return index.compareTo(other.getCoef());
}
}
}
package homework.poly;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Objects;
/**
* Poly Expression.
* <p>
* contains a sort of poly terms
*/
public class PolyExp implements Derivable {
private HashMap<BigInteger, PolyTerm> terms;
public PolyExp() {
this(16);
}
public PolyExp(int initialCapacity) {
terms = new HashMap<>(initialCapacity);
}
/**
* Shouldn't use this one to create a new PolyExp
* <p>
* you should use PolyExp(int, int) or
* PolyExp(BigInteger, BigInteger) instead
*
* @param term initial PolyTerm
*/
PolyExp(PolyTerm term) {
this();
terms.put(term.getIndex(), term);
}
public PolyExp(BigInteger index, BigInteger coef) {
this(new PolyTerm(index, coef));
}
public PolyExp(int index, int coef) {
this(new PolyTerm(index, coef));
}
public PolyExp add(PolyExp other) {
other.getTerms().forEach((k, v) -> terms.merge(k, v, PolyTerm::add));
return this;
}
PolyExp add(PolyTerm term) {
BigInteger index = term.getIndex();
if (terms.containsKey(index)) {
PolyTerm termExisted = terms.get(index);
PolyTerm termAdded = termExisted.add(term);
terms.replace(index, termAdded);
} else {
terms.put(index, term);
}
return this;
}
private HashMap<BigInteger, PolyTerm> getTerms() {
return terms;
}
@Override
public PolyExp getDerivative() {
PolyExp derExp = new PolyExp();
terms.forEach((k, v) -> derExp.add(v.getDerivative()));
return derExp;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
PolyExp exp = (PolyExp) o;
return toSortedString().equals(exp.toSortedString());
}
/**
* to sorted string
*
* @return sorted string
*/
@Override
public String toString() {
return toSortedString();
}
private String toSortedString() {
StringBuilder builder = new StringBuilder();
ArrayList<PolyTerm> list = new ArrayList<>(terms.values());
list.sort(PolyTerm::compareTo);
list.forEach(builder::append);
String res = builder.toString();
if (res.isEmpty()) {
res = "0";
}
if (res.charAt(0) == '+') {
res = res.substring(1);
}
return res;
}
@Override
public int hashCode() {
return Objects.hash(terms);
}
}