算术题生成器

后缀表达式

中缀表达式是将运算符放在操作数中间,而后缀表达式则是将运算符放在操作数后面,同时也不需要括号,直接上个例子:

中缀表达式 后缀表达式
(a+b)*c-(d+e)/f ab+c*de+f/-

你会发现没有括号的后缀表达式非常适合计算机来处理:

  1. 准备一个栈
  2. 从左至右遍历表达式
    • 遍历时遇到操作数则入栈
    • 遍历时运算符则出栈一定数量的操作数,将运算结果入栈
  3. 当表达式遍历完成,栈中应当只有一个数,即为表达式结果

既然后缀表达式这么好用,那么我们的问题就在于如何将中缀表达式简简洁高效地转换成后缀表达式,如果和模拟法一样复杂,那么后缀表达式就没有意义。

基于栈

中缀表达式的复杂之处在于其括号,要匹配括号最高效地办法就是利用栈,在这之上改进就可以转换成后缀表达式。

从左至右遍历表达式:

  1. 遇到操作数直接输出
  2. 遇到运算符则分为以下情况:
    1. 栈为空:该运算符直接入栈
    2. 栈顶为左括号:该运算符直接入栈
    3. 栈顶运算符优先级较低:该运算符直接入栈
    4. 栈顶运算符优先级较高或相等:出栈并输出,再次比较该运算符与栈顶运算符
  3. 遇到左括号则直接入栈
  4. 遇到右括号则依次出栈并输出,直至遇到左括号,将左括号出栈,与右括号一同舍弃
  5. 遍历完成,出栈并输出所有运算符

当然上述只限于二元运算符,如果有一元运算符则需要做出一定改动,但是并不影响整体结构。

同时,以上输出运算符时其实可以直接进行计算,没必要等表达式处理完毕再从头遍历开始计算,输出时直接压入另一个栈就好。

括号法

上面说到中缀表达式的复杂之处在于其括号,运算符有优先级再加上括号改变优先级才是其复杂的原因,如果我们让优先级只有一套规则,没有特权就会简单许多,于是我们可以在不改变表达式运算优先级的情况下,在所有子表达式外都加上括号,就可以很轻松地转换成后缀表达式。

还是以 (a+b)*c-(d+e)/f 为例:

  1. 加括号变成:(((a+b)*c)-((d+e)/f))
  2. 将运算符移动至当前子表达式的括号右侧:(((ab)+c)*((de)+f)/)-
  3. 去掉所有括号:ab+c*de+f/-

括号法复杂的点在加括号上,后面两步加起来只要一个遍历就能完成,但是我没有实现过,故不作评论。

语法树

不得不说树在哪里都能插上一手。我们按照以下规则构建一棵二叉树:

  1. 叶子节点都是操作数
  2. 非叶子节点都是运算符
  3. 根节点的优先级低于子节点

如果构建出该树,则通过先序/中序/后序遍历,我们分别可以得到前缀/中缀/后缀表达式(二叉树🐮🍺)。

从中缀表达式构建树可能会比较复杂,因为需要统计所有运算符的优先级(括号会改变优先级),从优先级最低的运算符开始拆解表达式,递归处理。本人没有实现过,也就不妄作评论了。

生成表达式

纵观上述中缀表达式转后缀表达式的方法,我们发现如果有一个后缀表达式,或者一棵语法树,那么就能非常轻松地得到中缀表达式。

基于栈

首先随机生成一定数量的操作数和二元运算符,保证二元运算符数量等于操作数数量减一。原因很简单:我们最后要的表达式能计算成一个数,设一个数地权重为 1,那么表达式地总权重应当为 1。二元运算符会将两个数变成一个数,权重就是 -1。简单运算就能得到两者之间的数量关系。

接下来需要将操作数与运算符组合起来,同样使用上述的权重计算。我们知道后缀表达式从左只有计算,并且始终有至少一个操作数,即从后缀表达式的最左侧截取至任意位置,这个子表达式的权重不能小于 1。所以我们遍历可以插入运算符的位置,保证总权重不小于 1,每迭代一个操作数则总权重加 1,添加一个运算符则权重减 1,到最后时将剩下的运算符都插入即可。

一元运算符的权重为 0,所以可以任意添加,只要不添加在最左侧即可。

后缀表达式生成完成之后,则需要转换成中缀表达式,首先准备一个栈(存放 (操作数/表达式, 优先级)),然后遍历后缀表达式:

  1. 遇到操作数则入栈
  2. 遇到运算符则出栈一定数量操作数或表达式,根据优先级给需要的表达式加上括号,然后拼接成表达式,入栈

当遍历完成时,栈中应当只有一个表达式,即为结果。

如果需要同步计算出结果,栈中存放的元素应当为 (操作数/表达式, 优先级, 值)

上段代码:

def generate(level):
    """
    根据 level 生成指定等级的算术题
    0:小学;1:初中;2:高中
    """

    """
    生成操作数序列以及二元运算符序列
    """
    length = randint(0 if level else 1, 4)
    op2Arr = [Generator.opset[randint(0, 3)] for i in range(length)]
    numArr = [randint(1, 100) for i in range(length + 1)]

    """
    生成二元运算符的位置
    """
    remain = 1
    position = []
    for i in range(length):
        position.append(randint(0, remain))
        remain += 1 - position[i]
    if remain > 1:
        position[-1] += remain - 1

    """
    生成一元运算符序列
    """
    op1Arr = []
    if level:
        if level == 1:
            op1Arr.append(Generator.opset[randint(4, 5)])
        elif level == 2:
            op1Arr.append(Generator.opset[randint(6, 8)])
        for i in range(randint(0, level)):
            op1Arr.append(
                Generator.opset[randint(4, 5 if level == 1 else 8)])
        shuffle(op1Arr)

    """
    生成后缀表达式
    """
    expression = numArr
    offset = 2
    index = 0
    for i in range(length):
        for j in range(position[i]):
            expression.insert(i + j + offset, op2Arr[index])
            index += 1
        offset += position[i]
    for op in op1Arr:
        expression.insert(randint(1, len(expression)), op)

    def getPriority(item):
        """
        返回运算符或操作数的优先级
        操作数:0
        一元运算符:1
        '*'、'/':2
        '+'、'-':3
        """
        if isinstance(item, int):
            return 0
        elif item == '+' or item == '-':
            return 3
        elif item == '*' or item == '/':
            return 2
        else:
            return 1

    """
    转换成中缀表达式
    stack 存储 (expression, priority)
    """
    stack = []
    for e in expression:
        priority = getPriority(e)
        if priority == 0:
            """
            是一个操作数,直接入栈
            """
            stack.append((e, 0))
        elif priority == 3:
            """
            是加/减运算,优先级最低,拼接后直接入栈
            """
            item2 = stack.pop()[0]
            item1 = stack.pop()[0]
            stack.append(('%s%s%s' % (item1, e, item2), 3))
        elif priority == 2:
            """
            是乘/除运算,如果有加/减运算需要加括号
            """
            item2, prio2 = stack.pop()
            if prio2 > 2:
                item2 = '(%s)' % item2
            item1, prio1 = stack.pop()
            if prio1 > 2:
                item1 = '(%s)' % item1
            stack.append(('%s%s%s' % (item1, e, item2), 2))
        elif priority == 1:
            """
            是一元运算,除了操作数都要加括号
            """
            item, prio = stack.pop()
            if prio:
                item = '(%s)' % item
            if e == '²':
                stack.append(('%s%s' % (item, '²'), 1))
            else:
                stack.append(('%s%s' % (e, item), 1))

    return stack[0][0]

语法树

这种方法我没有实现,但是我可以直接给出节点的数据结构:

class Node {
    String expression;  // 表达式
    Double value;       // 值
    int priority;       // 优先级
    Node left;          // 左子节点
    Node right;         // 右子节点
}

根据规则生成一棵合法的树,就可以得到一个合法的表达式:

  1. 叶子节点都是操作数
  2. 非叶子节点都是运算符
  3. 根节点的优先级低于子节点
posted @ 2020-09-30 11:47  三尺青锋丶  阅读(698)  评论(0)    收藏  举报