oo第一单元作业--表达式解析与语法树

吴佳峻-22373141 学生 2024-03-18 22:14:03

oo第一单元作业--表达式解析与语法树

第一次作业架构分享

简介

分成表达式解析和多项式计算两个部分

这里觉得将解析与计算尽量解耦有利于后续迭代,而不是实验课中构建表达式树,找到每个符号节点然后算上去。或者嫌麻烦可以直接将factor中的tostring改成topoly,直接进行计算,但我觉得写代码跨度有点大就没有选择。

其实思路差不多,但感觉分开会清晰点。

img

uml

img

预处理

处理各种指导书中出现的情况,如

  • 删除空白
  • 删除前导0
  • 第一个符号为+-就加一个0
  • 将多个+-号合并
  • 删除*和^后的+
  • 括号后有+号

表达式解析

将中缀表达式转换为后缀表达式。

参考oolens公众号中的思路,运用递归下降,我这里是用lexer扫一遍句子,生成对应的tokens,之后再用parser去递归解析,最后在每个factor中的tostring具体实现后缀表达式的生成(就是先递归再加符号)。这一步完成后可以先测试一下。

lexer

初始化时候直接用for循环来遍历字符串一遍就好。

parser

  • 按照优先级从小到大往下解析,我分成了四部分,expr,term,pow,sum or var,见uml图。
  • 这里需要注意的就是+-同一优先级,因此parseExpr()需要额外传一个符号(lexer.peek())进去。
  • 因为经过了预处理,开始第一项不会有+/-,因此直接解析parseTerm()就好
  • 最后的parseFactor麻烦一点,需要遇到括号调用parseExpr,然后将每个变量和数字存起来。
  • 这一步将符号去掉了,但根据你存进去的变量类型,会在toString中将符号再加上,+/-需要传额外的识别信息ops,这其实就是大一学到的用两个栈将前缀表达式转化为中缀。
  • 从term开始每一项就要不是固定的上下级了,因此需用抽象接口factor来暂时不确定具体的对象,直到明确了是哪一级再进行加入。

factor

parser解析完会有一个expr,之后对其递归进行tostring就可以得到一个后缀字符串了。

sb.append(" ").append(iter.next().toString());
sb.append(" ").append(ops.get(pos++).content());

这里手动加了空格是为了之后变成多项式的时候好解析。

tips

我这一步完全没考虑BigInteger,在多项式计算中才将系数设为BigInteger,这一点也简化了思考。

多项式计算

计算

用正则表达式来匹配上一步生成的后缀表达式的每一项。

建立了单项式类mono和多项式类poly,在poly中实现了加减乘幂(可以写个快速幂)。然后用个栈顺着计算就好。

可拓展性

poly为了之后的拓展,建立了单项式类mono和多项式类poly,poly中没有采用容易化简的hashmap,而是arraylist,因为感觉添加了三更多函数后可能还是arraylist更好想一点,用底数或系数当key感觉怪怪的,有点割裂单项式了。

以三角函数为例,后面可以在mono中加个

private Poly trigArgument; // 三角函数的参数,即内部的多项式 

然后在poly中更新对应的操作方法,比如加个canMerge

private boolean canMerge(Mono m1, Mono m2) {
        // 检查指数、三角函数类型和内部多项式是否相同
        if (m1.getExp() != m2.getExp()) return false;
        if (m1.getTrigFunctionType() != m2.getTrigFunctionType()) return false;
        if (m1.getTrigFunctionType() != TrigFunctionType.NONE) {
            // 对于三角函数,还需要比较内部多项式
            return m1.getTrigArgument().equals(m2.getTrigArgument());
        }
        return true;
    }

化简

  • 多项式开头的0去掉
  • 将相同底数的单项式合并
  • 如果多项式项有正有负,第一项最好是正的
  • 如果单项式系数为0,则最终结果为0

ToString

计算完的多项式还需要去tostring,就是考虑每一个mono间需不需要添加+,以及底数和指数间的*,这里可以写个测评机来看是否少考虑了情况。

  • 如果单项式系数为1,则可以省略系数,简化为x^n

  • 如果单项式系数为-1,则可以省略系数,简化为x^-n

  • 如果单项式x的指数为0,则最终结果只输出系数

  • 如果单项式x的指数为1,则指数部分可以省略,简化为ax

    if (sb.length() > 0 && !coeStr.startsWith("-")) {
                    sb.append("+");
                }
                if (!Objects.equals(coeStr, "")
                        && !Objects.equals(expStr, "")
                        && !Objects.equals(coeStr, "-")) {
                    coeStr = coeStr + '*';
                }
    

测评机实现代码

这里采用将x带为131计算,中测

import random

def generate_whitespace():
    # 控制空白字符的随机生成
    return random.choice([' ', '\t', '']) * random.randint(0, 2)

def generate_big_integer(max_digits=10):
    # 生成一个大整数,长度可达max_digits位
    return str(random.randint(1, 9)) + ''.join(random.choice('0123456789') for _ in range(random.randint(0, max_digits - 1)))

def generate_small_integer(max_value=8):
    # 生成一个小整数,用于指数,最大值为8
    return str(random.randint(1, max_value))

def generate_exponent():
    # 生成指数部分,最大值为8
    return '^' + generate_whitespace() + generate_small_integer()

def generate_variable_factor():
    # 生成变量因子,可能带有指数
    return 'x' + (generate_exponent() if random.choice([True, False]) else '')

def generate_constant_factor():
    # 生成常数因子,可以是大整数
    return generate_big_integer()

def generate_factor(depth=0):
    choices = [generate_variable_factor, generate_constant_factor]
    if depth < 1:  # 限制括号层数至多1层
        choices.append(lambda: generate_expression_factor(depth + 1))
    return random.choice(choices)()

def generate_item(depth=0):
    item = generate_factor(depth)
    while random.choice([True, False]):
        item += generate_whitespace() + '*' + generate_whitespace() + generate_factor(depth)
        if len(item) > 25: break
    return item

def generate_expression(depth=0):
    expr = generate_item(depth)
    while random.choice([True, False]):
        expr += generate_whitespace() + random.choice(['+', '-']) + generate_whitespace() + generate_item(depth)
        if len(expr.replace(' ', '').replace('\t', '')) > 45: break
    return expr

def generate_expression_factor(depth=0):
    expr = generate_expression(depth + 1)
    exp_part = generate_exponent() if random.choice([True, False]) else ''
    return '(' + expr + ')' + (generate_whitespace() + exp_part if exp_part else '')

def clean_expression(expr):
    # 移除表达式中的所有空白字符以满足有效长度要求
    return expr.replace(' ', '').replace('\t', '')

def generate_valid_expression():
    while True:
        expr = generate_expression()
        cleaned_expr = clean_expression(expr)
        if 1 <= len(cleaned_expr) <= 50:
            return cleaned_expr

# 生成并打印优化后的表达式
print(generate_valid_expression())

z strong

import random

maxLen = 200

def generate_whitespace():
    return random.choice([' ', '\t', '']) * random.randint(1, 2)

def generate_big_integer(max_digits=10):  # 调整大整数的最大位数
    num = str(random.randint(1, 9)) + ''.join(random.choice('0123456789') for _ in range(random.randint(0, max_digits - 1)))
    sign = random.choice(['', '+', '-'])  # 添加对带符号整数的支持
    return sign + num

def generate_small_integer(max_value=8):
    return str(random.randint(0, max_value))  # 包括0,以处理特殊情况0^0

def generate_exponent():
    return '^' + generate_small_integer()
    # return '^' + generate_whitespace() + generate_small_integer()

def generate_variable_factor():
    return 'x' + (generate_exponent() if random.choice([True, False]) else '')

def generate_constant_factor():
    return generate_big_integer()

def generate_factor(depth=0):
    choices = [generate_variable_factor, generate_constant_factor]
    if depth < 4:
        choices.append(lambda: generate_expression_factor(depth + 1))
    return random.choice(choices)()

def generate_item(depth=0):
    item = generate_factor(depth)
    while len(item) < maxLen and random.choice([True, False]):
        item += '*' + generate_factor(depth)
    #   item += '*' + generate_whitespace() + generate_factor(depth)
    return item

def generate_expression(depth=0):
    expr = generate_item(depth)
    while len(expr) < maxLen and random.choice([True, False]):
        expr += random.choice(['+', '-']) + generate_item(depth)
        # expr += generate_whitespace() + random.choice(['+', '-']) + generate_whitespace() + generate_item(depth)
    return expr

def generate_expression_factor(depth=0):
    expr = generate_expression(depth + 1)
    exp_part = generate_exponent() if random.choice([True, False]) else ''
    return '(' + expr + ')' + exp_part
    # return '(' + expr + ')' + (generate_whitespace() + exp_part)

def clean_expression(expr):
    return expr.replace(' ', '').replace('\t', '')

def generate_valid_expression():
    while True:
        expr = generate_expression()
        cleaned_expr = clean_expression(expr)
        return cleaned_expr

# 生成并打印优化后的表达式
print(generate_valid_expression())

main.py

import os
import subprocess
from sympy import symbols, sympify

n = 5000  # 定义测试的次数

def calculate_expression(expression: str):
    x = symbols('x')  # 定义符号x
    expression_with_value = expression.replace('^', '**')  # 替换^为**以适应Python的乘方运算
    try:
        expr = sympify(expression_with_value)  # 使用sympify解析表达式
        result = expr.subs(x, 131)  # 代入x=131
        result = result.evalf()  # 计算表达式的数值结果
    except Exception as e:
        print(f"Error evaluating expression: {expression}. Error: {e}")
        result = None
    return result

def isEqual(expression: str, java_expression: str) -> bool:
    python_result = calculate_expression(expression)
    java_result = calculate_expression(java_expression)
    if python_result is None or java_result is None:
        return False, python_result, java_result
    return abs(python_result - java_result) < 1e-9, python_result, java_result

# 初始化计数器
correct_count = 0
error_count = 0

# 打开一个文件以记录错误信息
with open('wrong.txt', 'w') as wrong_file, open('expression.txt', 'w') as expr_file:
    for i in range(1, n + 1):  # 从1开始计数,以符合通常的编号方式
        # 运行Python脚本生成表达式并写入in.txt
        subprocess.run(['python', 'data_generate.py'], stdout=open('in.txt', 'w'))

        # 读取Python生成的表达式并写入expression.txt
        with open('in.txt', 'r') as f:
            expression = f.readline().strip()
        expr_file.write(f"{i}. Python Expression: {expression}\n")

        # 运行Java程序处理Python生成的表达式
        os.system('java -jar 1.jar < in.txt > out.txt')

        # 读取Java程序的输出并追加到expression.txt
        with open('out.txt', 'r') as f:
            java_expression = f.read().strip()
        expr_file.write(f"   Java Expression: {java_expression}\n")

        # 比较结果
        equal, python_result, java_result = isEqual(expression, java_expression)
        if not equal:
            error_count += 1
            wrong_file.write(f"Mismatch found. Original Expression: {expression}, Python result: {python_result}, Java expression result: {java_result}, Java expression: {java_expression}\n")
        else:
            correct_count += 1

        # 实时输出正确和错误的条数
        print(f"Correct: {correct_count}, Errors: {error_count}")

# 最后,输出总结信息
print(f"Final tally: Correct: {correct_count}, Errors: {error_count}")

下个sympy库就可以运行啦~

终版

import random

maxLen = 200

def generate_whitespace():
    return random.choice([' ', '\t', '']) * random.randint(1, 2)

def generate_big_integer(max_digits=10):  # 调整大整数的最大位数
    num = str(random.randint(1, 9)) + ''.join(random.choice('0123456789') for _ in range(random.randint(0, max_digits - 1)))
    sign = random.choice(['', '+', '-'])  # 添加对带符号整数的支持
    return sign + num

def generate_small_integer(max_value=8):
    return str(random.randint(0, max_value))  # 包括0,以处理特殊情况0^0

def generate_exponent():
    return '^' + generate_small_integer()
    # return '^' + generate_whitespace() + generate_small_integer()

def generate_variable_factor():
    return 'x' + (generate_exponent() if random.choice([True, False]) else '')

def generate_constant_factor():
    return generate_big_integer()

def generate_factor(depth=0):
    choices = [generate_variable_factor, generate_constant_factor]
    if depth < 4:
        choices.append(lambda: generate_expression_factor(depth + 1))
    return random.choice(choices)()

def generate_item(depth=0):
    item = generate_factor(depth)
    while len(item) < maxLen and random.choice([True, False]):
        item += '*' + generate_factor(depth)
    #   item += '*' + generate_whitespace() + generate_factor(depth)
    return item

def generate_expression(depth=0):
    expr = generate_item(depth)
    while len(expr) < maxLen and random.choice([True, False]):
        expr += random.choice(['+', '-']) + generate_item(depth)
        # expr += generate_whitespace() + random.choice(['+', '-']) + generate_whitespace() + generate_item(depth)
    return expr

def generate_expression_factor(depth=0):
    expr = generate_expression(depth + 1)
    exp_part = generate_exponent() if random.choice([True, False]) else ''
    return '(' + expr + ')' + exp_part
    # return '(' + expr + ')' + (generate_whitespace() + exp_part)

def clean_expression(expr):
    return expr.replace(' ', '').replace('\t', '')

def generate_valid_expression():
    while True:
        expr = generate_expression()
        cleaned_expr = clean_expression(expr)
        return cleaned_expr

# 生成并打印优化后的表达式
print(generate_valid_expression())

多人测评

n = 500  # 定义测试的次数,这里用一个较小的数字以便快速测试

def calculate_expression(expression: str):
    x = symbols('x')
    expression_with_value = expression.replace('^', '**')
    try:
        expr = sympify(expression_with_value)
        result = expr.subs(x, 131)
        result = result.evalf()
    except Exception as e:
        result = None
    return result


def isEqual(expression: str, java_expression: str) -> bool:
    python_result = calculate_expression(expression)
    java_result = calculate_expression(java_expression)
    if python_result is None or java_result is None:
        return False
    return abs(python_result - java_result) < 1e-9


def simplify_expression(expression: str) -> str:
    x = symbols('x')
    try:
        expr = sympify(expression.replace('^', '**'))
        simplified_expr = simplify(expr)
        expanded_expr = expand(simplified_expr)
        final_expr_str = str(expanded_expr).replace('**', '^').replace(' ', '')
        return final_expr_str
    except Exception as e:
        return expression


# 循环测试六个Java文件
import os
import subprocess
from sympy import symbols, sympify, simplify, expand

n = 500  # 定义测试的次数,这里用一个较小的数字以便快速测试

# 准备测试数据
test_data = []
for _ in range(n):
    # 假设data_generate.py输出的数据直接是我们需要的表达式
    proc = subprocess.run(['python', 'data_generate.py'], capture_output=True, text=True)
    expression = proc.stdout.strip()
    simplified_expression = simplify_expression(expression)
    test_data.append((expression, simplified_expression))


def print_progress_bar(iteration, total, prefix='', suffix='', decimals=1, length=50, fill='█'):
    """
    Call in a loop to create terminal progress bar
    @params:
        iteration   - Required  : current iteration (Int)
        total       - Required  : total iterations (Int)
        prefix      - Optional  : prefix string (Str)
        suffix      - Optional  : suffix string (Str)
        decimals    - Optional  : positive number of decimals in percent complete (Int)
        length      - Optional  : character length of bar (Int)
        fill        - Optional  : bar fill character (Str)
    """
    percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
    filled_length = int(length * iteration // total)
    bar = fill * filled_length + '-' * (length - filled_length)
    print(f'\r{prefix} |{bar}| {percent}% {suffix}', end='\r')
    if iteration == total:
        print()


# 程序状态记录
program_status = {}

# 循环测试六个Java文件
for i in range(1, 7):
    jar_file = f"{i}.jar"
    wrong_file_name = f"wrong_{i}.txt"
    error_found = False

    print(f"Testing {jar_file}...")
    print_progress_bar(0, n, prefix='Progress:', suffix='Complete', length=50)

    with open(wrong_file_name, 'w') as wrong_file:
        for j, (expression, simplified_expression) in enumerate(test_data, start=1):
            with open('in.txt', 'w') as f:
                f.write(expression + '\n')

            subprocess.run(f"java -jar {jar_file} < in.txt > out.txt", shell=True, check=True)

            with open('out.txt', 'r') as f:
                java_expression = f.read().strip()

            if not isEqual(simplified_expression, java_expression):
                error_found = True
                wrong_file.write(
                    f"Mismatch at line {j}. Python Expression: {expression}, Simplified Python Expression: {simplified_expression}, Java Expression: {java_expression}\n")

            print_progress_bar(j, n, prefix='Progress:', suffix='Complete', length=50)

    # 记录程序是否有错
    program_status[jar_file] = 'No errors' if not error_found else 'Errors found'

print("测试完毕。")

# 输出每个程序的状态
for jar_file, status in program_status.items():
    print(f"{jar_file}: {status}")

单人

import os
import subprocess
from sympy import symbols, sympify, simplify, expand

n = 5000  # 定义测试的次数

def calculate_expression(expression: str):
    x = symbols('x')  # 定义符号x
    expression_with_value = expression.replace('^', '**')  # 替换^为**以适应Python的乘方运算
    try:
        expr = sympify(expression_with_value)  # 使用sympify解析表达式
        result = expr.subs(x, 131)  # 代入x=131
        result = result.evalf()  # 计算表达式的数值结果
    except Exception as e:
        print(f"Error evaluating expression: {expression}. Error: {e}")
        result = None
    return result

def isEqual(expression: str, java_expression: str) -> bool:
    python_result = calculate_expression(expression)
    java_result = calculate_expression(java_expression)
    if python_result is None or java_result is None:
        return False
    return abs(python_result - java_result) < 1e-9

def simplify_expression(expression: str) -> str:
    x = symbols('x')
    try:
        # 使用sympify解析并化简表达式
        expr = sympify(expression.replace('^', '**'))
        simplified_expr = simplify(expr)

        # 使用expand尝试进一步展开表达式,减少括号
        expanded_expr = expand(simplified_expr)

        # 将**替换为^并移除空格,以及尽可能移除括号
        expanded_expr_str = str(expanded_expr).replace('**', '^').replace(' ', '')

        # 移除表达式中不必要的括号(简单示例,可能不适用于所有情况)
        expanded_expr_str_no_parentheses = expanded_expr_str.replace('(', '').replace(')', '')

        return expanded_expr_str_no_parentheses
    except Exception as e:
        print(f"Error simplifying expression: {expression}. Error: {e}")
        return expression  # 如果无法化简,返回原始表达式

# 初始化计数器和列表
correct_count = 0
error_count = 0
different_lines = []

# 打开一个文件以记录错误信息和所有生成的表达式
with open('wrong.txt', 'w') as wrong_file, open('expression.txt', 'w') as expr_file:
    for i in range(1, n + 1):
        subprocess.run(['python', 'data_generate.py'], stdout=open('in.txt', 'w'))

        with open('in.txt', 'r') as f:
            expression = f.readline().strip()
        simplified_expression = simplify_expression(expression)
        expr_file.write(f"{i}. Python Expression: {expression}\n")
        expr_file.write(f"   Simplified Expression: {simplified_expression}\n")

        os.system('java -jar 1.jar < in.txt > out.txt')

        with open('out.txt', 'r') as f:
            java_expression = f.read().strip()
        expr_file.write(f"   Java Expression: {java_expression}\n")

        # 检查表达式结果是否相等
        equal = isEqual(simplified_expression, java_expression)

        # 检查化简后的Python表达式与Java表达式的长度
        len_diff = abs(len(simplified_expression) - len(java_expression))
        if len_diff > 1 or (len_diff == 1 and len(java_expression) > len(simplified_expression)):
            different_lines.append(i)
        elif not equal:
            error_count += 1
            wrong_file.write(f"Result mismatch at line {i}. Python Expression: {expression}, Simplified Python Expression: {simplified_expression}, Java Expression: {java_expression}\n")
        else:
            correct_count += 1

        # 实时输出正确和错误的条数以及长度不一致的行号
        if different_lines:
            different_str = ", ".join(map(str, different_lines))
            print(f"Correct: {correct_count}, Errors: {error_count}, Different: {different_str}")
        else:
            print(f"Correct: {correct_count}, Errors: {error_count}")

# 最后,输出总结信息
print(f"Final tally: Correct: {correct_count}, Errors: {error_count}, Different: {', '.join(map(str, different_lines))}")

第一次作业架构重构——ast树和访问者模式

因为第一次完成代码后剩余时间较多,受到这篇博客的启发,打算对代码进行优化。原来的思路虽然好想好写,但是太过于臃肿也并不美观,迭代的时候会需要分开考虑,因此还是选择了构建ast树,再对树进行运算,写完发现代码很美观整洁,就分享一下思路。

img

ast

好处

1. 精确表示表达式结构

AST能够精确表示表达式中各元素之间的关系,包括操作符的优先级和操作数的分组。这种结构化表示使得后续对表达式的处理(比如求值或转换)基于一个明确和准确的基础。

2. 便于表达式求值

通过AST,可以直接按照树的结构递归地求值,从叶节点开始,逐步向上直到根节点。这种方式自然地遵循了数学表达式的求值规则,包括操作符的优先级和左右结合性。

之后的计算MonoNode都会变成PolyMode,这个强制类型转换有点烦,但是在引入了下文提到的访问者模式后代码变得十分清晰。

3. 支持表达式的修改和优化,便于拓展

在AST上进行表达式的修改和优化变得更加直接和方便。

大大地简化了所谓"基本项",只剩了两个类

BinaryOperationNodeMonoNode两种,这里MonoNode我觉得有很大的可拓展性,完全可以去存cos,sin更多的信息。

public class MonoNode extends AstNode {
    private final BigInteger coefficient;
    private final String variable;
    private final BigInteger exponent;
   
public class BinaryOperationNode extends AstNode {
    private final AstNode left;
    private final AstNode right;
    private final String operator;
4. 方便实现更加高级的功能

构建AST之后,实现诸如符号解析等编译器高级功能变得更加直接。AST为这些分析提供了必要的信息和结构基础。

5. 增强代码的可读性和可维护性

将复杂的表达式处理流程分解为构建AST和在AST上进行操作两个阶段,可以使整个处理过程更加模块化,代码的可读性和可维护性得到提升。

具体做法

首先还是lexer用来解析,不再赘述。

在parser的写作上,我们不用再去纠结每个节点到底是expr,term,pow,还是num,var,统统当作AstNode就好。只需要梳理清楚运算符的优先级就好,就像下面这这样,递归去构建树就好,调试也很清晰。

public AstNode parseExpr() {
        AstNode node = parseTerm();
        while (true) {
            .......
                AstNode right = parseTerm();
                String operator = op.getType() == Token.Type.PLUS ? "+" : "-";
                node = new BinaryOperationNode(node, right, operator);
            } else {
                break;
            }
        }
        return node;
    }

在构建完整个ast树后,最后会返回根节点,从这里开始递归计算就好。

访问者模式

在思考后,我使用了访问者模式来处理ast树,这里仅仅是我个人的思考,想法有误或者代码有误还请大家指正。

访问者模式

  • 用途:允许在不修改现有对象结构的情况下,为对象结构中的元素添加新的操作。
  • 场景:特别适用于操作复杂对象结构,如遍历并执行操作的AST、文档对象模型(DOM)等。

为什么使用访问者模式处理AST

  1. 分离操作和结构:AST的结构通常比较固定,但是我们可能需要对它执行多种不同的操作,如类型检查、代码生成、优化等。访问者模式允许我们将这些操作逻辑从AST节点中分离出来,使得增加新操作不需要修改节点的定义。
  2. 集中化操作逻辑:通过使用访问者模式,可以将特定操作的逻辑集中在一个访问者类中,而不是分散在各个AST节点类中。这样做使得代码更加模块化,更易于理解和维护。
  3. 增强扩展性:当需要对AST添加新的操作时,我们只需要添加一个新的访问者类,而不需要改动AST节点的实现。这样,即使AST结构很复杂,系统的扩展性也得到了保证。
  4. 避免类型判断:在不使用访问者模式的情况下,操作AST时可能需要大量的类型判断和强制类型转换。访问者模式通过提供acceptvisit方法,利用方法重载机制自动选择正确的操作,从而避免了显式的类型判断。

上面这几个优点在代码中能很好地体现。

具体实现

``main.java
PolyNode result = tree.accept(evaluator);
String resultString = PolyUtils.toString(result)

从main中可以看到,只调用类tree的accept方法,就计算出来了,下面来看看访问者模式是怎么样将不同的Node进行统一。

下面是实现的visitor接口

package ast;

import ast.poly.PolyNode;

public interface AstVisitor {
    PolyNode visit(MonoNode node);

    PolyNode visit(BinaryOperationNode node);

    PolyNode visit(PolyNode node);
}

可以发现,我们去访问的时候是各种Node,但是返回的总是我们需要的PolyNode类型。

public class PolyNode extends AstNode {
    private final HashSet<MonoNode> monos;

tips:我这里PolyNode中只有加减乘乘方,其他有关于化简的操作被我放到PolyUtils这个类中了。

回到visitor中,只需要在BinaryOperationNodeMonoNode加个accept方法就好

public PolyNode accept(AstVisitor visitor) {
        return visitor.visit(this);
    }
}

最后我们在EvaluatorVisitor中实现这三个具体的visit方法:

例如:

public PolyNode visit(MonoNode node) {
        PolyNode result = new PolyNode();
        result.addMono(node);
        return result;
}

而对于符号节点来说,我们只需要拿到左边和右边算出的polyNode,再调用polyNode中的就算方法就可以算出来了。

case "+":
return leftResult.add(rightResult);

通过这个模式,我们就有效的实现了所有节点的统一,在外面来看,他们只做了相同的一件事,就是在收到Vistor的访问申请后,进行了允许访问的动作,而访问本身——就是计算。

在我们的例子中,一个访问者evaluator从树的根节点进入,得到accept后进入两个子节点

PolyNode leftResult = node.getLeft().accept(this);
PolyNode rightResult = node.getRight().accept(this);

上面的this就是我们创建的evaluator,当他走到最底层,就会返回MonoNode,这样从下往上一走,我们的计算过程就完成了。当然在他走的路途中,他的每个分身都PolyNode,最后会合到一起就得到了一个结果多项式。

ps:polyNode中我用的是HashSet去存每一项,这样更符合每一项的的平等性

private final HashSet<MonoNode> monos;

以上就是我重构的思路。至于如何化简什么的其他同学说的都很好了。

第二次作业架构分享--hashcode比较

一.整体架构

img

  1. 解析输入:首先,读取输入中的自定义函数定义数量n,然后读取n行的自定义函数定义,最后读取待展开的表达式。这些步骤将通过InputProcessor类实现。
  2. 存储自定义函数:使用Definer类来存储自定义函数的定义和参数。当读取到自定义函数定义时,解析函数名、参数列表和函数体,存储到Definer中。
  3. 表达式预处理:使用Pre类处理待展开的表达式,去除多余的空格,简化表达式等。
  4. 解析表达式:通过Parser类解析预处理后的表达式,将其转换为AST。
  5. 处理自定义函数调用:在AST构建过程中,当遇到自定义函数调用时,使用Definer查找函数定义,将实参替换形参,然后将替换后的表达式解析为AST的一部分。
  6. 展开括号和简化表达式:遍历AST,展开所有括号,并按照给定的规则简化表达式。这一步需要考虑如何保留必要的括号以满足正确性判定的要求。
  7. 生成最终表达式:遍历简化后的AST,进行计算。
  8. 输出结果:将最终的表达式字符串输出。

二.exp的解析

1.一元操作符

我在ast树中又加入unaryOperationNode类来解析exp和负号——负号的解析出现在parserfactor

public class UnaryOperationNode extends AstNode {
    private final AstNode operand;
    private final String operator;  //exp,-,
case MINUS:
                AstNode pow = parsePow();
                return new UnaryOperationNode(pow, "negate");

exp节点的添加

private AstNode parseExpFunction() {
        expect(Token.Type.LPAREN);
        AstNode operand = parseExpr();
        expect(Token.Type.RPAREN);
        return new UnaryOperationNode(operand, "exp");
    }

注意这里的节点类型需要是^级别的

2.MonoNode的修改

经过思考,选择了如下的结构

public class MonoNode extends AstNode {
    private BigInteger coe; 
    private BigInteger varExp; 
    private HashMap<PolyNode, Integer> eMap; 

这里用hashmap不用arraylist的主要原因就是在多项式计算的时候,需要一个能比较单项式x指数和e指数的函数equals,可以通过hashcode的重写轻松实现,避免了繁琐的递归与遍历。

3.有关hashcodeequals

需要写polynodemononodeequalshashcode方法。这里使用其本身的自带的函数就可以很容易地实现了。

PolyNode

public boolean equals(Object o) {
       ....... this.monos.containsAll(polyNode.monos) && polyNode.monos.containsAll(this.monos);
    }
 @Override
public int hashCode() {
   return Objects.hash(monos);
}

MonoNode

public boolean equals(Object o) {
       .......
        return varExp == monoNode.varExp && Objects.equals(eMap, monoNode.eMap);
    }
public int hashCode() {
        return Objects.hash(varExp, eMap);
    }

嗯这需要这几行,多项式时候合并同类项最大的问题就解决啦。

MonoNodeequals 方法中,如果你使用 Objects.equals 来比较两个 eMap,Java 会自动调用 HashMapequals 方法来检查两个映射是否等价,即它们是否包含完全相同的键值对。因此,即使是不同的 PolyNode 对象,只要它们内部的 eMap 包含相同的键值对,这些 PolyNode 就会被判断为相等。

这个机制确保了即使 eMap 不是同一个对象实例,只要它们的内容相同(即包含相同的 PolyNode 和对应的指数),MonoNode 的相等性判断逻辑就会认为它们相等。这对于在多项式中正确合并同类项至关重要。

进行多项式计算时候出现了为0的项要记得删除。

这里合并的时候无疑是可以优化的,关键就在于要不要把指数后面的次方乘进去,这个的话哪种更短可能也需要比较,但总体能提出来的公因式指数越大越好。

三.自定义函数

1.解析时替换(构建AST时)

这里选择了对字符串进行替换,而不是直接将字符串替换为节点,这样的话可以将definerast树解耦,definer中仅仅进行了对字符串的处理与更改,返回替换后的字符串。而单纯对字符串的处理也更容易一些。

2.如何进行替换

替换通常涉及以下步骤:

  1. 建立映射: 在解析自定义函数定义时,建立一个从变量名到参数位置的映射表。
  2. 遍历AST: 在需要替换变量名为参数位置时,遍历AST。
  3. 替换节点: 字符串替换唯一要注意的点就是要给所有替换的东西加个()

3.注意

要记录括号层数避免传参错误;对x的替换问题(解决方法很多......)

与预处理时候就进行字符串替换相比,这个可以与parser公用一个lexer,只需要替换后更新其内容与pos即可。替换后要调用的应该是parseFactor()

parser中
// 调用Definer.callFunc替换函数体
String replacedExpression = Definer.callFunc(functionName, args);

......
    
lexer.updateInput(
        lexer.getInput().substring(0, startReplacePos - 2) + replacedExpression +
                lexer.getInput().substring(endReplacePos - 1), startReplacePos - 2);

// 重新解析更新后的表达式,用于调试
System.out.println(lexer.getInput()+lexer.getCurrentPos());
return parseFactor();

ps:我觉得叶佩霖那个使用exec进行比较的思路很好,sympy对本次作业内的都可以计算

换进来的函数可能有前面的符号!!!!

因此对函数表达式也要预处理,还要加一个加号解析

加个解析加号就可以避免预处理0了

poly.getMonoNodes().removeIf(mono -> mono.getCoefficient().equals(BigInteger.ZERO));

很关键!!!

private HashMap<PolyNode, BigInteger> mergeEMaps(HashMap<PolyNode, BigInteger> mapE1,
                                                 HashMap<PolyNode, BigInteger> mapE2) {
    HashMap<PolyNode, BigInteger> result = new HashMap<>();

    mapE1.forEach((key1, value1) -> mapE2.forEach((key2, value2) -> {
        PolyNode newKey = key1.add(key2);
        result.put(newKey, BigInteger.ONE);
    }));

    return result;
}
if (gcd != null && !gcd.equals(BigInteger.ONE) && !gcd.equals(BigInteger.ZERO)) {
    gcdPolyNode = new PolyNode();
    for (MonoNode monoNode : polyNode.getMonoNodes()) {
        BigInteger newCoe = monoNode.getCoefficient().divide(gcd);
        MonoNode gcdMono = new MonoNode(newCoe,monoNode.getVarExp(),monoNode.getEMap());
        gcdPolyNode.addMono(gcdMono);
    }
    eMap.entrySet().removeIf(en -> BigInteger.ONE.equals(en.getValue()));
    eMap.put(gcdPolyNode, gcd);
}

深浅克隆背大锅

测评机

maxFuncLen = 15
maxLen = 20  # 20 已经足够长了
maxDepth = 3
pairs = []

import random


def generate_special_character():
    # 在这里可以扩展更多特殊字符
    special_characters = ['(0)', '(-0)', '(1)', '(-1)', 'exp(0)', 'exp(-0)', '(0-0)','(1+exp((1)^2))',
                          '(exp(0)^0)']

    #special_characters = ['(0)', '(-0)', '(1)', '(-1)', 'exp(-0)', , '((0)^0)''exp(0)', '((-0)^0)', '((0)^0)', '(exp(0)^0)'
                          #'(exp(-0)^0)']  # 例子中只包括0,可以根据需要添加更多
    return random.choice(special_characters)


# 修改generate_big_integer以包含特殊字符的生成
def generate_big_integer(max_digits=1, include_special=False):  # 添加include_special参数
    if include_special and random.choice([True, False]):
        # 一半机会返回一个特殊字符
        return generate_special_character()
    else:
        num = str(random.randint(1, 9)) + ''.join(
            random.choice('0123456789') for _ in range(random.randint(0, max_digits - 1)))
        sign = random.choice(['', '+', '-'])  # 添加对带符号整数的支持
        return sign + num


def generate_small_integer(max_value=2):
    return str(random.randint(0, max_value))  # 包括0,以处理特殊情况0^0


def generate_exponent():
    return '^' + generate_small_integer()


def generate_power_function(selected_symbol='x'):
    return random.choice(selected_symbol) + (generate_exponent() if random.choice([True, False]) else '')


def generate_trigonometric_function(Len=maxLen, mode='CLOSE', selected_symbol='x', depth=0):
    return random.choice(['sin', 'cos']) + "(" + generate_factor(Len - 3, mode, selected_symbol, depth + 1) + ")" + (
        generate_exponent() if random.choice([True, False]) else '')


def generate_exponential_function(Len=maxLen, mode='CLOSE', selected_symbol='x', depth=0):
    return "exp(" + generate_factor(Len - 3, mode, selected_symbol, depth + 1) + ")" + (
        generate_exponent() if random.choice([True, False]) else '')


def generate_variable_factor(selected_symbol='x'):
    return random.choice(selected_symbol) + (generate_exponent() if random.choice([True, False]) else '')


# 修改generate_constant_factor以随机选择生成特殊字符或常数
def generate_constant_factor():
    if random.choice([True, False]):  # 随机选择生成特殊字符或正常的大整数
        return generate_special_character()
    else:
        return generate_big_integer(max_digits=2, include_special=True)  # 确保特殊字符也可能在这里生成


def generate_custom_function(Len=maxLen, depth=0):
    if len(pairs) == 0:
        return ""

    pair = random.choice(pairs)  # 随机选择一个 pair
    function_name, cnt = pair

    if cnt == 1:
        return function_name + "(" + generate_expression(Len - 3, 'OPEN', 'x', depth + 1) + ")"
    else:
        return function_name + "(" + ','.join(
            [generate_expression(Len - 3, 'OPEN', 'x', depth + 1) for _ in range(cnt)]) + ")"


def generate_factor(Len=maxLen, mode='CLOSE', selected_symbol='x', depth=0):
    # 当达到最大深度时,只从不需要进一步递归的选项中选择
    if depth >= maxDepth:
        choices = [lambda: generate_variable_factor(selected_symbol), generate_constant_factor]
    else:
        choices = [lambda: generate_variable_factor(selected_symbol), generate_constant_factor,
                   lambda: generate_expression_factor(Len - 2, mode, selected_symbol, depth + 1),
                   lambda: generate_exponential_function(Len, mode, selected_symbol, depth + 1)]
        if mode == 'OPEN' and len(pairs) > 0:
            choices.extend([lambda: generate_custom_function(Len, depth + 1)])
    return random.choice(choices)()


def generate_item(Len=maxLen, mode='CLOSE', selected_symbol='x', depth=0):
    item = generate_factor(Len, mode, selected_symbol, depth)
    flag = 0
    while len(item) < Len and random.choice([True, False]) and depth < maxDepth:
        flag = 1
        item += '*' + generate_factor(Len - len(item), mode, selected_symbol, depth)
    if flag == 1:
        return "(" + item + ")"
    return item


def generate_expression(Len=maxLen, mode='CLOSE', selected_symbol='x', depth=0):
    expr = generate_item(Len, mode, selected_symbol, depth)
    flag = 0
    while len(expr) < Len and random.choice([True, False]) and depth < maxDepth:
        expr += random.choice(['+', '-']) + generate_item(Len - len(expr), mode, selected_symbol, depth)
        flag = 1
    if flag == 1:
        return "(" + expr + ")"
    else:
        return expr


def generate_expression_factor(Len=maxLen, mode='CLOSE', selected_symbol='x', depth=0):
    # 确保深度参数在递归调用时正确递增
    if depth < maxDepth:
        expr = generate_expression(Len, mode, selected_symbol, depth + 1)
        exp_part = generate_exponent() if random.choice([True, False]) else ''
        return '(' + expr + ')' + exp_part
    else:
        # 达到最大深度时的处理逻辑
        return generate_variable_factor(selected_symbol)


def clean_expression(expr):
    return expr.replace(' ', '').replace('\t', '')


def generate_valid_expression(len=maxLen, mode='CLOSE', selected_symbol='x'):
    expr = generate_expression(len, mode, selected_symbol)
    cleaned_expr = clean_expression(expr)
    return cleaned_expr


def is_contain_all_symbols(string, selected_symbol):
    for symbol in selected_symbol:
        if symbol not in string:
            return False
    return True


def generate_data():
    origin_function = ['f', 'g', 'h']
    used_function = []
    origin_symbols = ['x', 'y', 'z']
    n = random.randint(0, 3)
    print(n)
    for i in range(n):
        function_name = random.choice(origin_function)
        used_function.append(function_name)
        origin_function.remove(function_name)
        cnt = random.randint(1, 3)
        selected_symbol = random.sample(origin_symbols, cnt)
        random.shuffle(selected_symbol)
        pairs.append((function_name, cnt))
        tmp = generate_valid_expression(maxFuncLen, 'CLOSE', selected_symbol).replace('exp', 'e')
        while is_contain_all_symbols(tmp, selected_symbol) == False:
            tmp = generate_valid_expression(maxFuncLen, 'CLOSE', selected_symbol).replace('exp', 'e')
        print(function_name + "(" + ",".join(selected_symbol) + ")=" + tmp.replace("e", "exp"))
    s = generate_valid_expression(maxLen, 'OPEN')
    # print(used_function)
    while len(pairs) > 0 and is_contain_all_symbols(s, used_function) == False:
        s = generate_valid_expression(maxLen, 'OPEN')
    print(s)  # 生成含有自定义函数的表达式


# 生成并打印优化后的表达式
generate_data()

多人成绩

import subprocess
from tqdm import tqdm
from sympy import symbols, sympify, simplify, expand, exp, parse_expr, sin, cos
import concurrent.futures

n = 10  # 定义测试的次数


def equals_timeout(expression1, java_expression):
    python_result = parse_expr(expression1)
    java_result = parse_expr(java_expression.replace('^', '**'))

    return python_result.equals(java_result)


def isEqual(expression2: str, java_expression: str):
    if len(java_expression) > 1000:  # 跳过比较
        return 2

    try:
        return equals_timeout(expression2, java_expression)
    except TimeoutError:  # 如果equals判断运行超时,则返回True
        return 2


def perform_task(i2, jar_index2, expression_str2: str):
    """执行单个任务,比如运行Java程序并进行表达式比较"""
    try:

        # 调用Java程序处理同一输入
        try:
            subprocess.run(['java', '-jar', f'{jar_index2}.jar'], stdin=open('in.txt', 'r'),
                           stdout=open('out.txt', 'w'), timeout=2)
        except subprocess.TimeoutExpired:
            print("Java execution timed out")
            return 2

        with open('out.txt', 'r') as f:
            java_expression = f.read().strip()

        equal = isEqual(expression_str2, java_expression)
        return equal
    except Exception as e:
        print(f"Error during task {i2}: {e}")
        return 2  # 发生错误时返回None


for jar_index in range(1, 7):
    correct_count = 0
    error_count = 0
    timeout_count = 0
    with open(f'{jar_index}_wrong'
              f'.txt', 'w') as wrong_file:
        for i in tqdm(range(1, 1888), desc=f'Evaluating jar {jar_index}'):
            # 以下是对每个表达式的处理逻辑
            # 请在这里按照原逻辑准备expression_str等变量
            subprocess.run(['python', 'data_generate.py'], stdout=open('in.txt', 'w'))
            with open('in.txt', 'r') as file1:
                in_contents = file1.read()
            # with open('expression.txt', 'a') as file2:
            #     file2.write(str(i) + ".\n")
            #     file2.write(in_contents + '\n')

            with open('in.txt', 'r') as file:
                lines = file.readlines()

                # 读取函数定义数量
            n = int(lines[0].strip())

            # 定义可能用到的符号
            x, y, z = symbols('x y z')
            # 动态执行函数定义,并更新局部字典
            for line in lines[1:n + 1]:
                function_name, expression = line.split('=')

                # 去除空格
                function_name = function_name.strip()
                expression = expression.strip()

                # 替换符号和幂运算
                expression = expression.replace('^', '**')

                # 拼接输出字符串
                output_str = f"def {function_name}:return {expression}"

                exec(output_str)
                # print(f"定义了函数: {function_name} = {expression}")

                # 读取要化简的表达式并替换 ^ 为 **
            expression_str = lines[n + 1].strip()

            if len(expression_str) > 48:
                # print("too long")
                continue
            else:
                pass
                # print(f"处理的表达式: {expression_str}")

            # exec("expression_str = " + expression_str.replace('^', '**'))
            # 现在,确保表达式是正确的格式
            try:
                exec("expression_str = " + expression_str.replace('^', '**'))  # 这里尝试执行可能导致问题的代码
                expression_str = str(expression_str)
            except (ValueError, OverflowError, TypeError) as e:
                # 如果是已知可能的错误类型,打印错误并跳过当前迭代
                print(f"Skipping due to error: {e}")
                continue

            # print(f"Processing expression: {expression}")

            # 使用ThreadPoolExecutor管理并行任务
            with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
                # 提交任务
                future = executor.submit(perform_task, i, jar_index, expression_str)
                try:
                    # 等待任务完成,超时设置为4秒
                    result = future.result(timeout=4)
                    if result == 1:
                        correct_count += 1
                        continue
                    elif result == 0:
                        error_count += 1
                        wrong_file.write(
                            f"{in_contents}{expression_str}\n")
                        continue
                    else:  # None表示超时或错误
                        timeout_count += 1
                        wrong_file.write("time\n" +
                                         f"{in_contents}\n")
                        continue
                except concurrent.futures.TimeoutError:
                    print(f"Task {i} timed out.")
                    wrong_file.write(
                        f"{in_contents}\n")
                    timeout_count += 1
                    continue

        print(
            f"Final Results for jar {jar_index}: Correct: {correct_count}, Errors: {error_count}, "
            f"Timeouts: {timeout_count}. Check '{jar_index}_wrong.txt' for mismatches.")

第三次作业架构分享--访问者模式的拓展及复杂度分析

自定义函数嵌套

上次作业用递归解决的话自然而然就能实现嵌套了

求导

对于我的架构来说,求导就相当于一套新的运算规则,而原本的所有运算规则都在EvaluatorVistor中实现,想要在解析到dx的时候改变规则,就用一个新的访问者去挨个解析不就好了吗。因此启发我来创建一个的访问者:DerivativeVistor中来实现所有的求导规则,只需要重写所有节点的visit函数即可。

调用的话就将dx当成一个一元运算节点就好,创建一个新的访问者,去用求导的运算规则一一访问节点。

如下EvaluatorVistor

case "dx":
                DerivativeVisitor derivativeVisitor = new DerivativeVisitor();
                return node.getOperand().accept(derivativeVisitor);

如下给出示例:DerivativeVistor

public PolyNode visit(BinaryOperationNode node) {
        switch (node.getOperator()) {
            case "+":
            case "-":
                ......
            case "*":
                PolyNode left = node.getLeft().accept(new EvaluatorVisitor());
                PolyNode right = node.getRight().accept(new EvaluatorVisitor());
                PolyNode leftDerive = left.accept(this);
                PolyNode rightDerive = right.accept(this);
                return leftDerive.mul(right).add(left.mul(rightDerive));

这个是对乘法这个二元运算符的法则重写,也是求导运算中的乘法法则。

 @Override
public PolyNode visit(MonoNode node)
 @Override
public PolyNode visit(PolyNode node)
 @Override
public PolyNode visit(BinaryOperationNode node)
 @Override
public PolyNode visit(UnaryOperationNode node)

可以发现,用这种方法使得结构十分清晰,将所有的运算操作集成到了一个visit的类中,也就是将运算与节点因子进行了解耦,节点本身只提供了一个visit和accept的接口。这一点就成功的避免了节点中写过多的方法让人眼花缭乱,也将所有的求导操作都集成到了一个类中,便于阅读及debug。

测评机

maxFuncLen = 10
maxLen = 50 # 20 已经足够长了
maxDepth = 3
maxDerivCnt = 10
DerivCnt = 0

import random

def generate_special_character():
    # 在这里可以扩展更多特殊字符
    special_characters = ['1', '-1', 'exp(-0)']  # 例子中只包括0,可以根据需要添加更多
    return random.choice(special_characters)


# 修改generate_big_integer以包含特殊字符的生成
def generate_big_integer(max_digits=1, include_special=False):  # 添加include_special参数
    if include_special and random.choice([True, False, False]):
        # 一半机会返回一个特殊字符
        return generate_special_character()
    else:
        num = str(random.randint(1, 9)) + ''.join(random.choice('0123456789') for _ in range(random.randint(0, max_digits - 1)))
        sign = random.choice(['', '+', '-'])  # 添加对带符号整数的支持
        return sign + num

def generate_small_integer(max_value=3):
    return str(random.randint(0, max_value))  # 包括0,以处理特殊情况0^0

def generate_exponent():
    return '^' + generate_small_integer()

def generate_power_function(selected_symbol='x'):
    return random.choice(selected_symbol) + (generate_exponent() if random.choice([True, False]) else '')

def generate_trigonometric_function(used_function, Len=maxLen, mode='CLOSE', selected_symbol='x', depth=0):
    return random.choice(['sin', 'cos']) + "(" + generate_factor(used_function, Len - 3, mode, selected_symbol, depth+1) + ")" + (generate_exponent() if random.choice([True, False]) else '')

def generate_exponential_function(used_function, Len=maxLen, mode='CLOSE', selected_symbol='x', depth=0):
    return "exp(" + generate_factor(used_function, Len-3, mode, selected_symbol, depth+1) + ")" + (generate_exponent() if random.choice([True, False]) else '')

def generate_variable_factor(selected_symbol='x'):
    return random.choice(selected_symbol) + (generate_exponent() if random.choice([True, False]) else '')

# 修改generate_constant_factor以随机选择生成特殊字符或常数
def generate_constant_factor():
    if random.choice([True, False]):  # 随机选择生成特殊字符或正常的大整数
        return generate_special_character()
    else:
        return generate_big_integer(max_digits=1, include_special=True)  # 确保特殊字符也可能在这里生成

def generate_custom_function(used_function, Len=maxLen, mode='CLOSE', depth=0):
    if len(used_function) == 0:
        return ""

    pair = random.choice(used_function)  # 随机选择一个 pair
    function_name, cnt = pair

    if cnt == 1:
        return function_name + "(" + generate_expression(used_function, Len-3, mode, 'x', depth+1) + ")"

    return function_name + "(" + ','.join([generate_expression(used_function, Len-3, mode, 'x', depth+1) for _ in range(cnt)]) + ")"

def generate_derivative_factor(used_function, Len=maxLen, mode='ClOSE', selected_symbol='x', depth=0):
    global DerivCnt
    DerivCnt += 1
    return 'dx(' + generate_expression(used_function, Len-2, mode, selected_symbol, depth) + ')'

def generate_factor(used_function, Len=maxLen, mode='CLOSE', selected_symbol='x', depth=0):
    # 当达到最大深度时,只从不需要进一步递归的选项中选择
    if depth >= maxDepth:
        choices = [lambda: generate_variable_factor(selected_symbol), generate_constant_factor]
    else:
        choices = [lambda: generate_variable_factor(selected_symbol), generate_constant_factor,
                   lambda: generate_expression_factor(used_function, Len-2, mode, selected_symbol, depth+1),
                   lambda: generate_exponential_function(used_function, Len, mode, selected_symbol, depth + 1)]
        if len(used_function) > 0:
            choices.extend([lambda: generate_custom_function(used_function, Len-2, mode, depth+1)])
        if DerivCnt < maxDerivCnt and mode == 'OPEN':
            choices.extend([lambda: generate_derivative_factor(used_function, Len-2, mode, selected_symbol, depth + 1)])
            choices.extend(
                [lambda: generate_derivative_factor(used_function, Len - 2, mode, selected_symbol, depth + 1)])
            choices.extend(
                [lambda: generate_derivative_factor(used_function, Len - 2, mode, selected_symbol, depth + 1)])

    return random.choice(choices)()

def generate_item(used_function, Len=maxLen, mode='CLOSE', selected_symbol='x', depth=0):
    item = generate_factor(used_function, Len, mode, selected_symbol, depth)
    flag = 0
    while len(item) < Len and depth < maxDepth and random.choice([True, False]):
        item += '*' + generate_factor(used_function, Len - len(item), mode, selected_symbol, depth)
        flag = 1
    if flag :return '(' + item + ')'
    return item

def generate_expression(used_function, Len=maxLen, mode='CLOSE',selected_symbol='x', depth=0):
    expr = generate_item(used_function, Len, mode, selected_symbol, depth)
    flag = 0
    while len(expr) < Len and depth < maxDepth and random.choice([True, False]):
        flag = 1
        expr += random.choice(['+', '-']) + generate_item(used_function, Len - len(expr), mode, selected_symbol, depth)
    if flag == 0 : return expr
    else : return '(' + expr + ')'

def generate_expression_factor(used_function, Len=maxLen, mode='CLOSE', selected_symbol='x', depth=0):
    # 确保深度参数在递归调用时正确递增
    if depth < maxDepth:
        expr = generate_expression(used_function, Len, mode, selected_symbol, depth + 1)
        exp_part = generate_exponent() if random.choice([True, False]) else ''
        return '(' + expr + ')' + exp_part
    else:
        # 达到最大深度时的处理逻辑
        return '(' + generate_variable_factor(selected_symbol) + ')'

def clean_expression(expr):
    return expr.replace(' ', '').replace('\t', '')

def generate_valid_expression(used_function, len = maxLen, mode='CLOSE',selected_symbol='x'):
    expr = generate_expression(used_function, len, mode, selected_symbol)
    cleaned_expr = clean_expression(expr)
    return cleaned_expr

def is_contain_all_symbols(string, selected_symbol):
    if len(selected_symbol) == 0:
        return True
    for symbol in selected_symbol:
        if symbol not in string:
            return False
    return True

def generate_data():
    origin_function = ['f', 'g', 'h']
    used_function = []
    used_function_name = []
    origin_symbols = ['x', 'y', 'z']
    n = random.randint(0, 3)
    print(n)
    for i in range(n):
        function_name = random.choice(origin_function)
        origin_function.remove(function_name)
        cnt = random.randint(1, 3)
        selected_symbol = random.sample(origin_symbols, cnt)
        random.shuffle(selected_symbol)
        tmp = generate_valid_expression(used_function, maxFuncLen, 'OPEN', selected_symbol).replace('exp', 'e')
        while is_contain_all_symbols(tmp, selected_symbol) == False or is_contain_all_symbols(tmp, used_function_name) == False:
            tmp = generate_valid_expression(used_function, maxFuncLen, 'OPEN', selected_symbol).replace('exp', 'e')
        used_function_name.append(function_name)
        used_function.append((function_name, cnt))
        print(function_name + "(" + ",".join(selected_symbol) + ")=" + tmp.replace("e", "exp"))
    s = generate_valid_expression(used_function, maxLen, 'OPEN')
    # print(used_function)
    while len(used_function) > 0 and is_contain_all_symbols(s, used_function_name) == False:
        s = generate_valid_expression(used_function, maxLen, 'OPEN')
    print('dx(' + s + ')') # 生成含有自定义函数的表达式

# 生成并打印优化后的表达式
generate_data()

架构总结

uml

img

架构

<>中为软件包

  1. 输入预处理 (InputProcessor, Pre): 输入预处理部分处理原始输入,包括函数定义和表达式。它负责去除空格、添加首个零、简化符号等,以标准化输入字符串。
  2. 解析过程词法分析 (Lexer): 将预处理后的字符串输入转换为一系列令牌(Tokens),为语法解析阶段准备数据。语法分析 (Parser): 使用词法分析器的输出构建一个抽象语法树(AST)。这个树结构表示了输入表达式的数学逻辑结构。
  3. AST节点 (AstNode 及其子类): 定义了表达式的基本构件,如单项式(MonoNode)、二元操作节点(BinaryOperationNode)、一元操作节点(UnaryOperationNode)等。
  4. 访问者模式 (AstVisitor 接口及其实现): 定义了操作AST的一系列方法,使得在不修改节点类的情况下,可以添加新的操作,如求值或求导。
  5. 函数定义与管理 (Definer): 管理用户定义的函数,允许在表达式中使用这些函数,po。
  6. 主程序 (Main): 整合上述组件,实现从输入到输出的完整流程。

访问者模式详解

访问者模式在这个项目中扮演了至关重要的角色,它通过定义一个外部访问者接口来操作组成表达式的各种元素,而不需要修改这些元素本身的代码。

访问者接口 (AstVisitor)
  • 定义了对不同类型AST节点进行操作的visit方法。每种节点类型都有其对应的visit方法。
具体访问者
  • 求值访问者 (EvaluatorVisitor): 这个访问者通过递归遍历AST,计算并返回表达式的值。对于二元操作节点,它会先计算左右子节点的值,然后根据操作符(加、减、乘、除)计算结果。对于一元操作节点,它会先计算子节点的值,然后应用一元操作(如取负、求指数,dx)。
  • 求导访问者 (DerivativeVisitor): 这个访问者遍历下面的astNode,实现了对表达式的符号微分。对于基本的数学操作(加、减、乘、除、指数),它应用相应的微分规则。对于更复杂的函数调用或表达式,需要递归地应用链式法则或乘积法则等。
访问者模式的优势
  • 分离关注点: 将操作逻辑(如求值、求导)从数据结构(AST节点)中分离出来,减少了代码间的耦合,增强了代码的可维护性。
  • 统一处理: 将操作逻辑与数据结构分离,不仅减少了各个类的复杂度,也使得代码更加清晰,操作的逻辑更易于理解和修改。
  • 增加新操作的灵活性,扩展性:如果需要对AST添加新的操作(例如,添加一个用于优化表达式的访问者),我们只需添加一个新的访问者类,而不需要修改AST节点的定义。这种方式使得系统更加模块化,易于扩展和维护。
  • 减少代码重复:在不使用访问者模式的情况下,如果需要对AST的不同节点执行相似的操作,可能会导致大量的代码重复。访问者模式通过在访问者中集中操作逻辑,可以有效避免这种情况。

应用挑战及应对策略

  • 处理复杂表达式: 通过构建复杂而灵活的AST结构,加上访问者模式的灵活操作,项目能有效处理多变的数学表达式。
  • 新增操作的简易性: 随着需求的增加,例如添加新的计算功能或支持新的表达式类型,访问者模式使得这些扩展变得简单,无需修改现有节点类代码。
  • 性能考量: 虽然访问者模式增加了一定的运行时开销(因为需要通过接口调用方法),但它提供的灵活性和可维护性在许多情况下是值得的。

bug分析

在三次作业中出现了一个bug,就是在parse中解析负号的逻辑不对,应该是到幂次级别,而不是因子级别。

复杂度分析

img

可以发现average都在合理的范围内,模块复杂度小于圈复杂度。

下面是按复杂度从高到低进行排序:

img

总体观察

  • 认知复杂度 (CogC): 考虑到认知复杂度是衡量代码理解难度的指标,高值通常指示代码逻辑复杂、难以理解。此列表中的函数显示了一系列认知复杂度,其中一些函数的认知复杂度相对较高,可能需要重点关注以简化或重构以提高可维护性。
  • 基本复杂度 (ev(G)): 基本复杂度衡量程序的非结构化程度。较高的值可能意味着程序中存在过多的跳转语句,导致代码逻辑难以跟踪。
  • 模块设计复杂度 (iv(G)): 模块设计复杂度反映了模块内部逻辑和模块间调用的复杂性。较高的值表明模块间的依赖关系和内部逻辑判断较为复杂,可能导致代码难以修改和测试。
  • 圈复杂度 (v(G)): 圈复杂度是衡量模块控制流程复杂度的指标,直接影响测试用例的编写。高圈复杂度意味着需要更多的测试用例来覆盖所有可能的路径,这可能增加测试工作的复杂性。

详细分析

  • PolyUtils.formatExpPolyUtils.toString 函数:这两个函数具有较高的认知复杂度和圈复杂度,表明它们在理解和测试上可能需要较大的努力。尤其是toString方法,具有高基本复杂度和模块设计复杂度,指出可能存在多个非结构化的控制流和复杂的模块调用关系。
  • **Pre.simplifySignsPre.deleteMulAdd**:这些函数有较低的认知复杂度和较低的模块设计复杂度,但其圈复杂度相对较高,表明尽管它们比较易于理解,但需要更多的测试用例来覆盖所有执行路径。
  • **Parser.parseFunctionCallPolyNode.addMono**:这些函数的认知复杂度和圈复杂度表明它们在逻辑上相对复杂且测试覆盖困难。特别是parseFunctionCall,其模块设计复杂度较高,指示在模块间调用关系上可能较为复杂。

可以发现集中在了一些工具函数上,主要是对数据的预处理用了很多判断以及对最后结果的合并与exp的化简(我把排序,提公因式,一些小特判放在了tostring中)

Parser.parseFunctionCall主要是对自定义函数的替换逻辑,为了保证参数识别正确,用了一个判断括号的大循环和一些变量,虽然可以将这个再分出去一个函数,但觉得没有必要。

以上。

课程感想

学习心得体会

  1. 解析与计算解耦: 初始采取的将表达式解析和多项式计算分开的方法显著提升了迭代过程。与实验课中将这些组件交织在一起的做法不同,这种方法不仅简化了开发过程,而且使代码更易于管理和适应未来的修改。
  2. 使用lexerparser 采用lexer扫描句子和parser进行解析的做法,展现了通过递归下降的方式精准地将中缀表达式转换为后缀表达式的高效性。这种方法的逻辑清晰,易于实现,且为后续的多项式计算打下了坚实基础。
  3. 多项式计算的可扩展性: 通过建立单项式和多项式的类,实现加减乘幂等操作,不仅为当前的计算需求提供了解决方案,还考虑到了未来功能扩展的可能性。
  4. 化简和优化: 采用正则表达式匹配和动态数据结构(如HashSet)管理多项式中的单项式,使得相同底数的单项式合并、系数为0的项移除等化简操作变得简洁高效。

未来方向

课程改进建议:

  1. 加强基础知识讲解: 在第一单元中,可以增加对词法分析(lexer)和语法分析(parser)等编译原理基础知识的讲解,以帮助学生更好地理解表达式解析的背后逻辑。
  2. 提供更多实践机会: 增设更多实践练习,特别是与表达式解析、多项式计算等核心功能相关的项目实践,以加深学生对理论知识的应用理解。
  3. 互测分数多一点
...全文
256 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

发帖
与我相关
我的任务
社区描述
2023年北航面向对象设计与构造
学习 高校
社区管理员
  • YannaZhang
  • CajZella
  • C_ecelia
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧