【问题标题】:Problems with a shunting yard algorithm调车场算法的问题
【发布时间】:2011-03-08 23:41:12
【问题描述】:

我已经在java中成功实现了一个调车场算法。算法本身很简单,但是我在使用标记器时遇到了问题。目前,该算法适用于我想要的一切,不包括一件事。如何区分减法(-)和负数(-)

比如4-3就是减法 但 -4+3 是负数

我现在知道如何找出它什么时候应该是负数,什么时候应该是负数,但是它应该放在算法中的什么位置,因为如果你像函数一样使用它,它就不会总是起作用

3 + 4 * 2 / -( 1 − 5 ) ^ 2 ^ 3

当 1-5 变为 -4 时,它会在平方和立方之前变为 4

就像 3 + 4 * 2 / cos( 1 − 5 ) ^ 2 ^ 3 ,你会在平方和立方之前取余弦

但在真正的数学中你不会用 - 因为你真正想说的是 3 + 4 * 2 / -(( 1 − 5 ) ^ 2 ^ 3) 以获得正确的值

【问题讨论】:

  • 我添加了“java”标签,我认为它可能会让您的问题获得更多浏览量。

标签: java subtraction shunting-yard


【解决方案1】:

听起来您正在做一个 lex-then-parse 样式的解析器,您需要在词法分析器中使用一个简单的状态机,以便为一元和二进制减号获取单独的标记。 (在 PEG 解析器中,您不必担心这一点。)

在 JavaCC 中,您将有一个 DEFAULT 状态,您可以将 - 字符视为 UNARY_MINUS。当您标记主表达式的结尾时(根据您提供的示例,可以是右括号或整数),然后您将切换到INFIX 状态,其中- 将被视为INFIX_MINUS。一旦遇到任何中缀运算符,就会返回DEFAULT 状态。

如果您自己滚动,可能会比这更简单一些。看看这个Python code 是一个聪明的方法。基本上,当您遇到- 时,您只需检查前一个标记是否是中缀运算符。该示例使用字符串"-u" 来表示一元减号标记,这便于进行非正式标记化。最好我能说的是,Python 示例确实无法处理 - 跟随开放括号或出现在输入开头的情况。这些也应该被认为是一元的。

为了在调车场算法本身中正确处理一元减号,它需要比任何中缀运算符具有更高的优先级,并且需要标记为右关联。 (确保您处理右关联性。您可能已将其排除在外,因为其余的运算符都是左关联的。)这在 Python 代码中很清楚(尽管我会使用某种结构而不是两个单独的映射) .

当需要计算时,您需要稍微不同地处理一元运算符,因为您只需要从堆栈中弹出一个数字,而不是两个。根据您的实现情况,只需浏览列表并将每次出现的"-u" 替换为[-1, "*"] 可能会更容易。

如果您完全可以使用 Python,那么您应该能够在我链接的示例中看到我所说的所有内容。我发现代码比其他人提到的 C 版本更容易阅读。另外,如果你好奇的话,我不久前写了一篇关于使用 shunting-yard in Ruby 的文章,但我将一元运算符作为单独的非终结符处理,因此没有显示。

【讨论】:

    【解决方案2】:

    this question 的答案可能会有所帮助。

    特别是,其中一个答案引用了 C 中处理一元减号的 solution

    基本上,您必须根据在二元运算符不能出现的位置上出现的减号来识别一元减号,并为其制作不同的标记,因为它具有不同的优先级。

    Dijkstra 的 original paper 并没有太清楚地解释他是如何处理这个问题的,但一元减号被列为单独的运算符。

    【讨论】:

    • 标准的调车场算法不支持它们,我正在尝试修改它以支持它们。 Wolfram alpha, texas Instruments, wolfram mathematica, microsoft math 等.. 支持它们,所有这些都使用调车场算法的一个版本
    【解决方案3】:

    在你的词法分析器中,你可以实现这个伪逻辑:

    if (symbol == '-') {
        if (previousToken is a number 
         OR previousToken is an identifier 
         OR previousToken is a function) {
            currentToken = SUBTRACT;
        } else {
            currentToken = NEGATION;
        }
    }
    

    您可以设置否定的优先级高于乘法和除法,但低于求幂。您还可以将其设置为右关联(就像'^')。 然后你只需要按照维基百科页面上的描述将优先级和关联性集成到算法中。

    如果token是一个操作符,o1,那么:当有一个操作符时 令牌,o2,位于堆栈顶部,并且任一 o1 是左关联的 并且它的优先级小于等于o2,或者o1有 优先级小于 o2,将 o2 从堆栈中弹出,到输出 队列;将 o1 压入堆栈。

    我最终实现了这个相应的代码:

    } else if (nextToken instanceof Operator) {
        final Operator o1 = (Operator) nextToken;
    
        while (!stack.isEmpty() && stack.peek() instanceof Operator) {
            final Operator o2 = (Operator) stack.peek();
    
            if ((o1.associativity == Associativity.LEFT && o1.precedence <= o2.precedence)
             || (o1.associativity == Associativity.RIGHT && o1.precedence < o2.precedence)) {
                popStackTopToOutput();
            } else {
                break;
            }
        }
    
        stack.push(nextToken);
    }
    

    Austin Taylor 说得非常正确,您只需为一元运算符弹出一个数字:

    if (token is operator negate) {
        operand = pop;
        push operand * -1;
    }
    

    示例项目:

    https://github.com/Digipom/Calculator-for-Android/

    延伸阅读:

    http://en.wikipedia.org/wiki/Shunting-yard_algorithm

    http://sankuru.biz/blog/1-parsing-object-oriented-expressions-with-dijkstras-shunting-yard-algorithm

    【讨论】:

    • 这看起来不错,但一元减号应该比任何其他运算符具有更高的优先级
    【解决方案4】:

    这不是Java,但这是我在搜索并没有找到任何明确答案后专门为解决此问题而编写的一个库。 这可以满足您的所有需求,甚至更多:

    https://marginalhacks.com/Hacks/libExpr.rb/

    它是一个 ruby​​ 库(以及用于检查它的测试平台),它运行修改后的调车场算法,该算法还支持一元 ('-a') 和三元 ('a?b:c') 操作。它还执行 RPN、Prefix 和 AST(抽象语法树)——您的选择,并且可以评估表达式,包括产生可以处理任何变量评估的块(某种 lambda)的能力。只有 AST 可以完成全套操作,包括处理短路操作的能力(例如 '||' 和 '?:' 等),但 RPN 确实 支持一元。它还有一个灵活的优先级模型,包括 C 表达式或 Ruby 表达式(不一样)所做的优先级预设。测试台本身很有趣,因为它可以创建随机表达式,然后可以 eval() 并通过 libExpr 运行以比较结果。

    它有相当多的文档/评论,因此将这些想法转换为 Java 或其他语言应该不会太难。

    就一元运算符而言,基本思想是您可以根据前一个标记识别它们。如果前一个标记是运算符或左括号,则“可能的一元”运算符(+ 和 -)只是一元的,并且只能用一个操作数推送。重要的是,您的 RPN 堆栈区分一元运算符和二元运算符,以便它知道在评估时要做什么。

    【讨论】:

    • 为一个 12 岁的问题添加一个新答案,忽略问题中所请求的语言,并不会尝试实际回答提出的问题,但恰好与您的新问题的关键字匹配发布的工具只是垃圾邮件。
    • 这里的答案提供了有关如何得出解决方案的信息。问题的年龄无关紧要,因为仍然没有任何普遍可用的解决方案。
    • (更清楚地说,我写了这个问题是因为这个stackoverflow问题以及我在任何地方都找不到通用解决方案)
    • 我已经明确表示写了它,它是为了响应需要解决这个确切的问题。顺便说一句,你的链接“容易找到”似乎并不能帮助我真正找到任何东西,但我相信你。
    • 谢谢。这改善了很多。赞成。
    【解决方案5】:

    我知道这是一篇旧帖子,但可能有人会觉得它有用。 我之前实现过这个算法,从使用 StreamTokenizer 类的 toknizer 开始 它工作正常。在 Java 的 StreamTokenizer 中,有一些具有特定含义的字符。例如:( 是一个运算符,sin 是一个词,... 对于您的问题,有一个名为“streamToknizer.ordinaryChar(..)”的方法,它指定字符参数在此标记器中是“普通的”。它删除了字符作为注释字符、单词组件、字符串分隔符、空格或数字字符的任何特殊意义。来源here

    所以你可以将 - 定义为普通字符,这意味着它不会被视为数字的符号。例如,如果你有表达式 2-3 ,你将有 [2,-,3],但是如果您没有将其指定为普通,因此它将是 [2,-3]

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-09-29
      • 1970-01-01
      • 2016-07-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多