diff --git a/src/AbstractProblemGenerator.java b/src/AbstractProblemGenerator.java index 4c84464..9576e3d 100644 --- a/src/AbstractProblemGenerator.java +++ b/src/AbstractProblemGenerator.java @@ -1,13 +1,15 @@ import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Random; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; /** * 抽象题目生成器(模板方法模式)。 - * 实现了 IProblemGenerator 接口,提供了通用的题目生成逻辑, - * 如唯一性检查循环、操作数生成等,将具体的题目构建逻辑延迟到子类。 + * 提供了生成题目的通用流程和创建基础四则运算的核心方法。 */ public abstract class AbstractProblemGenerator implements IProblemGenerator { @@ -17,51 +19,153 @@ public abstract class AbstractProblemGenerator implements IProblemGenerator { protected static final int MIN_OPERANDS_COUNT = 2; protected static final int MAX_OPERANDS_COUNT = 5; + private static final Operator[] AVAILABLE_OPERATORS = { + Operator.ADD, Operator.SUBTRACT, Operator.MULTIPLY, Operator.DIVIDE + }; + @Override - public List generate(int count, Set existingProblems) { + public final List generate(int count, Set existingProblems) { List newProblems = new ArrayList<>(count); - // 使用一个临时的Set来防止在同一次生成任务中产生重复 Set currentBatchSet = new HashSet<>(); - while (newProblems.size() < count) { - Equation candidate = createProblem(); + int maxTries = count * 100; + int tries = 0; + while (newProblems.size() < count && tries < maxTries) { + Equation candidate = createProblem(); String canonicalForm = candidate.toCanonicalString(); - // 执行唯一性检查:既不能与历史重复,也不能与本次生成的其他题目重复 - if (!existingProblems.contains(canonicalForm) && !currentBatchSet.contains(canonicalForm)) { + if (!existingProblems.contains(canonicalForm) && currentBatchSet.add(canonicalForm)) { newProblems.add(candidate); - currentBatchSet.add(canonicalForm); } + tries++; } return newProblems; } /** - * 创建一个候选数学题目的抽象方法(模板方法)。 - * 子类必须实现此方法来定义具体难度的题目生成规则。 - * - * @return 一个新创建的 Equation 对象 + * 模板方法:创建一道具体问题的抽象定义,由子类实现。 */ protected abstract Equation createProblem(); /** - * 生成一个在指定范围内的随机整数。 + * 创建一个基础的四则运算方程式。 + * 此方法已被重构,将复杂逻辑委托给私有辅助方法。 * - * @param min 最小值(包含) - * @param max 最大值(包含) - * @return 随机整数 + * @param nonNegativeOnly 如果为 true,则强制保证所有中间和最终结果非负。 + * @return 一个 Equation 对象。 */ - protected int getRandomNumber(int min, int max) { - return random.nextInt(max - min + 1) + min; + protected Equation createBaseArithmeticProblem(boolean nonNegativeOnly) { + int numOperands = getRandomNumber(MIN_OPERANDS_COUNT, MAX_OPERANDS_COUNT); + List operands = new ArrayList<>(); + List operators = new ArrayList<>(); + + int currentResult = getRandomNumber(MIN_OPERAND_VALUE, MAX_OPERAND_VALUE); + operands.add(String.valueOf(currentResult)); + + for (int i = 0; i < numOperands - 1; i++) { + if (nonNegativeOnly) { + currentResult = appendNonNegativeOperation(operands, operators, currentResult); + } else { + appendStandardOperation(operands, operators); + } + } + + addParentheses(operands, operators); + return new Equation(operands, operators); + } + + /** + * (Refactored Helper) 追加一个确保结果非负的运算步骤。 + * + * @param operands 当前操作数列表 + * @param operators 当前操作符列表 + * @param currentResult 当前的累计计算结果 + * @return 更新后的累计计算结果 + */ + private int appendNonNegativeOperation(List operands, List operators, int currentResult) { + Operator operator = AVAILABLE_OPERATORS[random.nextInt(AVAILABLE_OPERATORS.length)]; + int nextOperand; + int newResult = currentResult; + + switch (operator) { + case ADD: + nextOperand = getRandomNumber(MIN_OPERAND_VALUE, MAX_OPERAND_VALUE); + newResult += nextOperand; + break; + case SUBTRACT: + nextOperand = getRandomNumber(MIN_OPERAND_VALUE, currentResult); + newResult -= nextOperand; + break; + case MULTIPLY: + nextOperand = getRandomNumber(MIN_OPERAND_VALUE, 10); + newResult *= nextOperand; + break; + case DIVIDE: + List divisors = findDivisors(currentResult); + nextOperand = divisors.get(random.nextInt(divisors.size())); + newResult /= nextOperand; + break; + default: + // 为了编译安全,实际上不会执行到这里 + throw new IllegalStateException("Unexpected value: " + operator); + } + operands.add(String.valueOf(nextOperand)); + operators.add(operator); + return newResult; } /** - * 从给定的操作符数组中随机选择一个。 - * @param availableOperators 可用的操作符数组 - * @return 随机选择的操作符 + * (Refactored Helper) 追加一个标准的运算步骤(允许负数)。 + * + * @param operands 当前操作数列表 + * @param operators 当前操作符列表 */ - protected Operator getRandomOperator(Operator[] availableOperators) { - return availableOperators[random.nextInt(availableOperators.length)]; + private void appendStandardOperation(List operands, List operators) { + Operator operator = AVAILABLE_OPERATORS[random.nextInt(AVAILABLE_OPERATORS.length)]; + int nextOperand; + + if (operator == Operator.DIVIDE) { + // 为确保整除,反向构造 + int divisor = getRandomNumber(1, 10); + int quotient = getRandomNumber(1, 20); + // 替换前一个操作数以确保可整除 + operands.set(operands.size() - 1, String.valueOf(divisor * quotient)); + nextOperand = divisor; + } else { + nextOperand = getRandomNumber(MIN_OPERAND_VALUE, MAX_OPERAND_VALUE); + } + operands.add(String.valueOf(nextOperand)); + operators.add(operator); + } + + + private void addParentheses(List operands, List operators) { + if (operators.size() > 1 && random.nextBoolean()) { + int pos = random.nextInt(operators.size()); + Operator op = operators.get(pos); + // 仅在优先级较低的运算符旁边添加括号才有视觉意义 + if (op == Operator.ADD || op == Operator.SUBTRACT) { + operands.set(pos, "(" + operands.get(pos)); + operands.set(pos + 1, operands.get(pos + 1) + ")"); + } + } + } + + private List findDivisors(int number) { + if (number == 0) { + return Collections.singletonList(1); // 避免除以0 + } + return IntStream.rangeClosed(1, Math.abs(number)) + .filter(i -> number % i == 0) + .boxed() + .collect(Collectors.toList()); + } + + protected int getRandomNumber(int min, int max) { + if (min > max) { + return max; // 安全处理,例如当currentResult变为1时 + } + return random.nextInt(max - min + 1) + min; } } \ No newline at end of file diff --git a/src/Application.java b/src/Application.java index 4ed397f..30e86ff 100644 --- a/src/Application.java +++ b/src/Application.java @@ -128,6 +128,7 @@ public class Application { System.out.println("正在生成 " + count + " 道不重复的新题目..."); IProblemGenerator generator = sessionManager.getCurrentGenerator(); List newProblems = generator.generate(count, history); + // 命令行显示生成题目 for (int i = 0; i < newProblems.size(); i++) { String problemLine = (i + 1) + ". " + newProblems.get(i).toString(); diff --git a/src/ElementaryProblemGenerator.java b/src/ElementaryProblemGenerator.java index 50cef20..26e8a0b 100644 --- a/src/ElementaryProblemGenerator.java +++ b/src/ElementaryProblemGenerator.java @@ -1,45 +1,12 @@ -import java.util.ArrayList; -import java.util.List; - /** * 小学题目生成器(具体策略)。 - * 生成包含2-5个操作数的四则运算题目。 + * 其实现委托给基类的核心方法,并强制要求结果非负。 */ public class ElementaryProblemGenerator extends AbstractProblemGenerator { - private static final Operator[] AVAILABLE_OPERATORS = { - Operator.ADD, Operator.SUBTRACT, Operator.MULTIPLY, Operator.DIVIDE - }; - @Override protected Equation createProblem() { - int numOperands = getRandomNumber(MIN_OPERANDS_COUNT, MAX_OPERANDS_COUNT); - List operands = new ArrayList<>(); - List operators = new ArrayList<>(); - - // 生成第一个操作数 - operands.add(getRandomNumber(MIN_OPERAND_VALUE, MAX_OPERAND_VALUE)); - - for (int i = 0; i < numOperands - 1; i++) { - Operator operator = getRandomOperator(AVAILABLE_OPERATORS); - int nextOperand; - - // 特殊处理除法,确保除数不为0且能整除,避免产生小数 - if (operator == Operator.DIVIDE) { - // 为了简化,我们让被除数是新生成的随机数的倍数 - int divisor = getRandomNumber(1, 10); // 将除数范围缩小以增加整除概率 - int quotient = getRandomNumber(1, 10); - int dividend = operands.get(operands.size() - 1); // 获取前一个结果或操作数 - operands.set(operands.size() - 1, divisor * quotient); // 替换前一个操作数确保能整除 - nextOperand = divisor; - } else { - nextOperand = getRandomNumber(MIN_OPERAND_VALUE, MAX_OPERAND_VALUE); - } - - operands.add(nextOperand); - operators.add(operator); - } - - return new Equation(operands, operators, true); + // 调用父类的核心方法,并传入 true,要求启用“无负数”约束。 + return createBaseArithmeticProblem(true); } } \ No newline at end of file diff --git a/src/Equation.java b/src/Equation.java index 3bf80a9..76fcd70 100644 --- a/src/Equation.java +++ b/src/Equation.java @@ -1,77 +1,82 @@ -import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; +import java.util.stream.IntStream; /** * 方程式实体类。 - * 封装了构成一个数学方程式的所有元素(操作数、操作符)及其行为(计算、格式化输出)。 + * 封装了构成一个数学方程式的所有元素(操作数、操作符)。 + * 操作数统一使用 String 类型,以支持数字、函数表达式等多种形式。 */ public class Equation { - private final List operands; // 使用String以支持如 "sqrt(16)" 的形式 + private final List operands; private final List operators; + /** + * 构造函数。 + * + * @param operands 操作数列表(可以是数字、函数表达式等) + * @param operators 操作符列表 + */ public Equation(List operands, List operators) { - this.operands = new ArrayList<>(operands); - this.operators = new ArrayList<>(operators); - } - - // 为了方便,提供一个只接受整数操作数的构造函数 - public Equation(List intOperands, List operators, boolean isSimple) { - this.operands = intOperands.stream().map(String::valueOf).collect(Collectors.toList()); - this.operators = new ArrayList<>(operators); + this.operands = Collections.unmodifiableList(operands); + this.operators = Collections.unmodifiableList(operators); } + /** + * 获取操作数列表的不可变视图。 + * + * @return 操作数列表 + */ public List getOperands() { - return Collections.unmodifiableList(operands); + return operands; } + /** + * 获取操作符列表的不可变视图。 + * + * @return 操作符列表 + */ public List getOperators() { - return Collections.unmodifiableList(operators); + return operators; } /** - * 生成用于展示给用户和保存到文件的标准字符串。 - * 格式: "操作数1 运算符1 操作数2 = 结果" + * 生成用于展示给用户和保存到文件的标准问题字符串。 + * 格式: "操作数1 运算符1 操作数2 = ?" * * @return 格式化的方程式字符串 */ @Override public String toString() { StringBuilder sb = new StringBuilder(); - sb.append(operands.get(0)); - for (int i = 0; i < operators.size(); i++) { - sb.append(" ").append(operators.get(i).getSymbol()).append(" "); - sb.append(operands.get(i + 1)); - } - sb.append(" = ").append("?"); + // 使用 IntStream 来优雅地处理操作数和操作符的交替拼接 + IntStream.range(0, operators.size()) + .forEach(i -> sb.append(operands.get(i)) + .append(" ") + .append(operators.get(i).getSymbol()) + .append(" ")); + sb.append(operands.get(operands.size() - 1)); // 追加最后一个操作数 + sb.append(" = ?"); return sb.toString(); } /** * 生成用于唯一性校验的“规范化”字符串。 - * 这是确保 "3 + 5" 和 "5 + 3" 被视为同一题目的关键。 - * 规范化规则:对于可交换运算符,操作数按升序排列。 + * 对于简单的可交换运算 (如 "3 + 5"),操作数会排序以确保唯一性 ("3 + 5")。 + * 对于复杂或不可交换的运算,直接返回原始顺序的字符串。 * * @return 规范化的方程式字符串 */ public String toCanonicalString() { - if (operators.size() == 1 && operators.get(0).isCommutative()) { - // 仅处理最简单的二元运算场景 - String[] sortedOperands = operands.toArray(new String[0]); - Arrays.sort(sortedOperands); - return sortedOperands[0] + " " + operators.get(0).getSymbol() + " " + sortedOperands[1]; + // 简化处理:仅对两个操作数且操作符可交换的情况进行规范化 + if (operands.size() == 2 && operators.size() == 1 && operators.get(0).isCommutative()) { + List sortedOperands = operands.stream().sorted().collect(Collectors.toList()); + return sortedOperands.get(0) + " " + operators.get(0).getSymbol() + " " + sortedOperands.get(1); } - // 对于更复杂的或不可交换的运算,直接按顺序拼接 - StringBuilder sb = new StringBuilder(); - sb.append(operands.get(0)); - for (int i = 0; i < operators.size(); i++) { - sb.append(" ").append(operators.get(i).getSymbol()).append(" "); - sb.append(operands.get(i + 1)); - } - return sb.toString(); + // 对于其他所有复杂情况,直接按原样拼接 + return toString().replace(" = ?", ""); } } \ No newline at end of file diff --git a/src/HighSchoolProblemGenerator.java b/src/HighSchoolProblemGenerator.java index 22739ba..ff3bdcc 100644 --- a/src/HighSchoolProblemGenerator.java +++ b/src/HighSchoolProblemGenerator.java @@ -3,52 +3,34 @@ import java.util.List; /** * 高中题目生成器(具体策略)。 - * 作为初中生成器的子类,它首先生成一个包含初中难度运算(平方/开方)的题目, + * 它首先生成一个包含初中难度运算(平方/开方)的题目, * 然后再额外加入一个三角函数运算,使得题目难度递增。 */ public class HighSchoolProblemGenerator extends MiddleSchoolProblemGenerator { - // 预定义一些常见的三角函数角度 private static final int[] PREDEFINED_ANGLES = {0, 30, 45, 60, 90}; + private static final String[] TRIG_FUNCTIONS = {"sin", "cos", "tan"}; @Override protected Equation createProblem() { - // 首先,调用父类(MiddleSchoolProblemGenerator)的方法, - // 生成一个已经包含平方或开方运算的题目结构。 + // 1. 调用父类(已修正的 MiddleSchoolProblemGenerator)的方法 Equation middleSchoolEquation = super.createProblem(); - // 直接获取父类生成的可变列表进行再次修改 List operands = new ArrayList<>(middleSchoolEquation.getOperands()); List operators = new ArrayList<>(middleSchoolEquation.getOperators()); - // 随机选择一个操作数,用三角函数表达式替换。 - // 这可能替换一个普通数字,也可能替换掉父类生成的 sqrt(x) 或 (x^2) 表达式,增加了多样性。 + // 2. 随机替换一个操作数为三角函数 int modifyIndex = random.nextInt(operands.size()); int angle = PREDEFINED_ANGLES[random.nextInt(PREDEFINED_ANGLES.length)]; + String funcType = TRIG_FUNCTIONS[random.nextInt(TRIG_FUNCTIONS.length)]; - // 随机选择一个三角函数 - int funcType = random.nextInt(3); - String trigExpression; - - switch (funcType) { - case 0: // sin - trigExpression = "sin(" + angle + ")"; - break; - case 1: // cos - trigExpression = "cos(" + angle + ")"; - break; - default: // tan - if (angle == 90) { - angle = 45; // 避免tan(90)无定义的情况 - } - trigExpression = "tan(" + angle + ")"; - break; + if ("tan".equals(funcType) && angle == 90) { + angle = 45; // 避免 tan(90) } - // 用三角函数的表达式字符串替换随机选择的操作数 + String trigExpression = String.format("%s(%d)", funcType, angle); operands.set(modifyIndex, trigExpression); - // 用修改后的操作数和原有的操作符列表创建最终的高中题目 return new Equation(operands, operators); } } \ No newline at end of file diff --git a/src/MiddleSchoolProblemGenerator.java b/src/MiddleSchoolProblemGenerator.java index 19ae647..83f57c3 100644 --- a/src/MiddleSchoolProblemGenerator.java +++ b/src/MiddleSchoolProblemGenerator.java @@ -1,37 +1,34 @@ import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; /** * 初中题目生成器(具体策略)。 - * 基于小学题目,并确保至少包含一个平方或开方运算。 + * 首先生成一个允许出现负数的标准四则运算题目,然后确保至少包含一个平方或开方运算。 */ -public class MiddleSchoolProblemGenerator extends ElementaryProblemGenerator { +public class MiddleSchoolProblemGenerator extends AbstractProblemGenerator { @Override protected Equation createProblem() { - // 首先,借用父类方法生成一个基础的四则运算结构 - Equation basicEquation = super.createProblem(); + // 1. 调用父类的核心方法,并传入 false,允许生成包含负数的标准表达式。 + Equation basicEquation = createBaseArithmeticProblem(false); - // 将父类生成的操作数(Integer)转换为本类需要的 String 列表 - List operands = basicEquation.getOperands().stream() - .map(String::valueOf) - .collect(Collectors.toList()); + // 2. 将基础表达式元素转为可修改的列表 + List operands = new ArrayList<>(basicEquation.getOperands()); List operators = new ArrayList<>(basicEquation.getOperators()); - // 随机选择一个操作数进行变形 + // 3. 随机选择一个操作数进行变形 int modifyIndex = random.nextInt(operands.size()); - int originalValue = Integer.parseInt(operands.get(modifyIndex)); + String targetOperand = operands.get(modifyIndex).replace("(", "").replace(")", ""); - // 随机决定是进行平方还是开方运算 + // 4. 随机决定是进行平方还是开方运算 if (random.nextBoolean()) { - // 平方运算:生成 "(originalValue^2)" 格式的字符串 - String squaredExpression = String.format("(%d^2)", originalValue); + // 平方运算 + String squaredExpression = String.format("(%s^2)", targetOperand); operands.set(modifyIndex, squaredExpression); } else { - // 开方运算:生成 "sqrt(value)" 格式的字符串 + // 开方运算 int base = getRandomNumber(2, 10); - int valueToRoot = base * base; // 确保可以完美开方 + int valueToRoot = base * base; String sqrtExpression = String.format("sqrt(%d)", valueToRoot); operands.set(modifyIndex, sqrtExpression); } diff --git a/src/Operator.java b/src/Operator.java index 8597142..2af0b79 100644 --- a/src/Operator.java +++ b/src/Operator.java @@ -1,54 +1,42 @@ -import java.util.function.BinaryOperator; - /** * 数学运算符枚举。 - * 封装了运算符的符号和其对应的计算逻辑。 - * 这种设计符合开闭原则,易于扩展新的运算符。 + * 封装了运算符的符号及其属性,如是否为可交换运算符。 */ public enum Operator { - ADD('+', (a, b) -> a + b, true), - SUBTRACT('-', (a, b) -> a - b, false), - MULTIPLY('*', (a, b) -> a * b, true), - DIVIDE('/', (a, b) -> a / b, false); + ADD('+', true), + SUBTRACT('-', false), + MULTIPLY('*', true), + DIVIDE('/', false); private final char symbol; - private final BinaryOperator operation; private final boolean isCommutative; // 标记是否为可交换运算符(如加法、乘法) /** * 构造函数。 * * @param symbol 运算符的字符表示 - * @param operation 封装了计算逻辑的函数式接口 * @param isCommutative 运算符是否满足交换律 */ - Operator(char symbol, BinaryOperator operation, boolean isCommutative) { + Operator(char symbol, boolean isCommutative) { this.symbol = symbol; - this.operation = operation; this.isCommutative = isCommutative; } + /** + * 获取运算符的字符表示。 + * + * @return 符号字符 + */ public char getSymbol() { return symbol; } - public boolean isCommutative() { - return isCommutative; - } - /** - * 应用此运算符进行计算。 + * 判断此运算符是否满足交换律。 * - * @param a 第一个操作数 - * @param b 第二个操作数 - * @return 计算结果 + * @return 如果是可交换的,则返回 true */ - public int apply(int a, int b) { - // 为除法增加安全检查,防止除以零 - if (this == DIVIDE && b == 0) { - // 在实际生成中,应避免生成除数为0的题目,这里作为最后防线 - throw new ArithmeticException("Division by zero."); - } - return operation.apply(a, b); + public boolean isCommutative() { + return isCommutative; } } \ No newline at end of file