使用多态性的表达式评估和树行走? (阿拉史蒂夫耶格)

Posted

技术标签:

【中文标题】使用多态性的表达式评估和树行走? (阿拉史蒂夫耶格)【英文标题】:Expression Evaluation and Tree Walking using polymorphism? (ala Steve Yegge) 【发布时间】:2010-09-05 23:10:39 【问题描述】:

今天早上,我在阅读Steve Yegge's: When Polymorphism Fails 时遇到了一个问题,他的一位同事过去常常在潜在员工来亚马逊面试时问他们。

作为多态性的一个例子 行动,一起来看看经典 “eval”面试问题,其中(如 据我所知)被带到亚马逊 通过罗恩布劳恩斯坦。问题是 相当丰富的,因为它设法 探索各种重要的 技能:OOP 设计、递归、二进制 树、多态性和运行时 打字、一般编码技能和(如果 你想让它变得更加困难) 解析理论。

在某个时候,候选人希望 意识到你可以代表一个 作为二进制的算术表达式 树,假设你只使用 二元运算符,例如“+”、“-”、 “*”、“/”。叶节点都是 数字,内部节点是 所有运营商。评估 表达的意思是走树。如果 候选人没有意识到这一点, 你可以轻轻地把他们带到那里,或者如果 有必要,直接告诉他们。

即使你告诉他们,它仍然是一个 有趣的问题。

问题的前半部分,其中 有些人(我会取他们的名字 保护我垂死的呼吸,但他们 缩写是威利刘易斯)感觉是 职位要求如果你想打电话 你自己是一名开发人员并工作于 亚马逊,其实有点难。这 问题是:你如何从一个 算术表达式(例如在 字符串),例如“2 + (2)” 表达树。我们可能有一个 ADJ 在某些方面对这个问题提出挑战 点。

后半部分是:假设这是 一个 2 人的项目,和你的伙伴, 我们称之为“威利”的人是 负责改造 字符串表达式到树中。你得到 最简单的部分:你需要决定什么 类威利是构建 树与。你可以在任何 语言,但一定要选择一种, 或者威利会交给你组装 语言。如果他觉得脾气暴躁,那 将用于不支持的处理器 生产时间更长。

您会惊讶于有多少候选人 boff 这个。

我不会给出答案,但是 标准不良解决方案涉及使用 switch 或 case 语句(或只是 好的老式级联ifs)。一种 稍微好一点的解决方案包括 使用函数指针表, 和可能是最好的解决方案 涉及使用多态性。一世 鼓励你克服它 有时。有趣的东西!

所以,让我们尝试通过所有三种方式来解决这个问题。你如何从算术表达式(例如在字符串中)例如“2 + (2)”到使用级联-if、函数指针表和/或多态性的表达式树?

随意解决一个、两个或所有三个。

[更新:修改标题以更好地匹配大多数答案。]

【问题讨论】:

基于 Mark Harrisson 的回答,我写了一个 php 实现 【参考方案1】:

String Tokenizer + LL(1) Parser 将为您提供一个表达式树...多态方式可能涉及一个带有“evaluate(a,b)”函数的抽象算术类,该函数被涉及的每个运算符覆盖(加法,减法等)返回适当的值,并且树包含整数和算术运算符,可以通过树的后(?)顺序遍历来评估。

【讨论】:

【参考方案2】:

应该使用函数式语言 imo。在 OO 语言中,树更难表示和操作。

【讨论】:

真的吗?这是天真的 C++ 实现:class AST vector child;无效推送(AST*); /* 添加子节点,应该从 yacc/bison 解析器 /AST eval() 中调用; / 递归计算子节点/ string dump(int=0); / 以带有标签的树形形式转储 */ ; 但是你在 eval() 正文中:当你尝试像 nest[0] /* lchild */ = nest[0]->eval() 那样做天真的 eval 时,很容易获得记忆Nest[0] 对象在评估之前泄漏。如果几个表达式之间共享变量,我真的不知道如何跟踪它,但是可以删除叶子编号。 我忘记了 'string val' 作为 AST 中节点本身的标签【参考方案3】:

或许这是真正的问题: 您如何将 (2) 表示为 BST? 那是让我绊倒的部分 起来。

递归。

【讨论】:

【参考方案4】:

我认为问题在于我们需要解析圆括号,但它们不是二元运算符?我们是否应该将 (2) 作为单个标记,计算结果为 2?

括号不需要出现在表达式树中,但它们确实会影响其形状。例如,(1+2)+3 的树与 1+(2+3) 不同:

    +
   / \
  +   3
 / \
1   2

    +
   / \
  1   +
     / \
    2   3

括号是解析器的“提示”(例如,根据 superjoe30,“递归下降”)

【讨论】:

【参考方案5】:

回复:贾斯汀

我认为这棵树看起来像这样:

  +
 / \
2  ( )
    |
    2

基本上,你会有一个“eval”节点,它只评估它下面的树。然后将其优化为:

  +
 / \
2   2

在这种情况下,不需要括号,也不添加任何内容。他们没有在逻辑上添加任何东西,所以他们就走了。

【讨论】:

【参考方案6】:

这涉及到解析/编译器理论,这有点像兔子洞……The Dragon Book 是编译器构造的标准文本,并将其发挥到了极致。在这种特殊情况下,您想为基本算术构造一个context-free grammar,然后使用该语法解析出一个abstract syntax tree。然后,您可以遍历树,从下往上减少它(此时您将应用多态性/函数指针/switch 语句来减少树)。

我发现 these notes 在编译器和解析理论方面非常有用。

【讨论】:

这是原始问题的最小 CFG:S -> N N -> 2 N -> N O N -> ( N ) O -> - N【参考方案7】:

多态树行走,Python 版本

#!/usr/bin/python

class Node:
    """base class, you should not process one of these"""
    def process(self):
        raise('you should not be processing a node')

class BinaryNode(Node):
    """base class for binary nodes"""
    def __init__(self, _left, _right):
        self.left = _left
        self.right = _right
    def process(self):
        raise('you should not be processing a binarynode')

class Plus(BinaryNode):
    def process(self):
        return self.left.process() + self.right.process()

class Minus(BinaryNode):
    def process(self):
        return self.left.process() - self.right.process()

class Mul(BinaryNode):
    def process(self):
        return self.left.process() * self.right.process()

class Div(BinaryNode):
    def process(self):
        return self.left.process() / self.right.process()

class Num(Node):
    def __init__(self, _value):
        self.value = _value
    def process(self):
        return self.value

def demo(n):
    print n.process()

demo(Num(2))                                       # 2
demo(Plus(Num(2),Num(5)))                          # 2 + 3
demo(Plus(Mul(Num(2),Num(3)),Div(Num(10),Num(5)))) # (2 * 3) + (10 / 2)

测试只是使用构造函数构建二叉树。

程序结构:

抽象基类:Node

所有节点都继承自这个类

抽象基类:BinaryNode

所有二元运算符都继承自此类 process 方法执行评估表达式并返回结果的工作

二元运算符类:Plus、Minus、Mul、Div

两个子节点,左侧和右侧子表达式各一个

数字类:Num

保存叶节点数值,例如17 或 42

【讨论】:

这个答案被过度设计了。问题是针对 2 + (2),而不是任意算术计算。此外,这只是执行树,而不是构建它。 问题是算术计算,例如 2 + (2),而不是 2 + (2) 的计算。因此,它没有过度设计,而是按预期回答了问题。 这个答案不是针对“你如何从算术表达式(例如,在字符串中)如“2 + (2)”......“演示(加(Mul(Num(2),Num(3)),Div(Num(10),Num(5))))" 来自?我们看不到的其他程序?为什么这被标记为最佳答案? “你得到了简单的部分:你需要决定 Willie 用什么类来构建树。”【参考方案8】:

@贾斯汀:

看看我关于表示节点的注释。如果您使用该方案,那么

2 + (2)

可以表示为

           .
          / \
         2  ( )
             |
             2

【讨论】:

【参考方案9】:

表示节点

如果要包含括号,我们需要5种节点:

二元节点:Add Minus Mul Div这些有两个子节点,一个左侧和一个右侧

     +
    / \
node   node

一个保存值的节点:Val没有子节点,只有一个数值

跟踪括号的节点:Paren子表达式的单个子节点

    ( )
     |
    node

对于多态解决方案,我们需要有这种类关系:

节点 BinaryNode : 从 Node 继承 加:从二进制节点继承 减号:从二进制节点继承 Mul : 从二进制节点继承 Div : 从二进制节点继承 值:从节点继承 Paren : 从节点继承

所有节点都有一个名为 eval() 的虚函数。如果您调用该函数,它将返回该子表达式的值。

【讨论】:

没有理由在抽象语法树中包含括号。 在某些情况下有。您可能有一个工具来重写/重新创建原始表达式,而不是优化原始表达式中的冗余。当然,对于评估表达式并得到答案的情况,您是正确的。【参考方案10】:

我不会给出答案,但是 标准不良解决方案涉及使用 switch 或 case 语句(或只是 好的老式级联ifs)。一种 稍微好一点的解决方案包括 使用函数指针表, 和可能是最好的解决方案 涉及使用多态性。

解释器过去 20 年的演变可以看作是另一种方式——多态性(例如朴素的 Smalltalk 元循环解释器)到函数指针(朴素的 lisp 实现、线程代码、C++)到切换(朴素的字节码解释器),然后是 JIT 等等——它们要么需要非常大的类,要么(在单多态语言中)双重调度,这将多态性降低为类型案例,你又回到了第一阶段。这里使用的“最佳”定义是什么?

对于简单的东西,多态解决方案是可以的 - here's one I made earlier,但如果你要绘制一个包含几千个数据点的函数,那么堆栈和字节码/开关或利用运行时的编译器通常会更好。

【讨论】:

【参考方案11】:

我认为问题在于如何编写解析器,而不是评估器。或者更确切地说,如何从字符串创建表达式树。

返回基类的 case 语句不完全计数。

“多态”解决方案的基本结构(换句话说,我不在乎你用什么构建它,我只是想通过重写尽可能少的代码来扩展它)是反序列化一个对象来自具有一组(动态)已知类型的流的层次结构。

实现多态解决方案的关键是有一种方法可以从模式匹配器创建表达式对象,可能是递归的。即,将 BNF 或类似语法映射到对象工厂。

【讨论】:

【参考方案12】:

正如人们之前提到的,当您使用表达式树时,不需要括号。当您查看表达式树时,操作顺序变得微不足道且显而易见。括号是对解析器的提示。

虽然公认的答案是解决问题的一半,但另一半——实际上是解析表达式——仍未解决。通常,这类问题可以使用recursive descent parser 来解决。编写这样的解析器通常是一个有趣的练习,但大多数 modern tools for language parsing 会为您抽象出它。

如果您在字符串中允许浮点数,解析器也显着更难。我必须创建一个 DFA 来接受 C 中的浮点数——这是一项非常艰苦和详细的任务。请记住,有效的浮点数包括:10, 10., 10.123, 9.876e-5, 1.0f, .025 等。我假设在采访中对此有所豁免(有利于简单明了)。

【讨论】:

【参考方案13】:

嗯...我认为您不能为此编写自上而下的解析器而无需回溯,因此它必须是某种移位减少解析器。 LR(1) 甚至 LALR 当然可以很好地使用以下(临时)语言定义:

开始 -> E1 E1 -> E1+E1 | E1-E1 E1 -> E2*E2 | E2/E2 | E2 E2 -> 号码 | (E1)

为了保持 * 和 / 高于 + 和 - 的优先级,必须将其分成 E1 和 E2。

但如果我必须手动编写解析器,我会这样做:

两个堆栈,一个存储树的节点作为操作数,一个存储运算符 从左到右读取输入,生成数字的叶节点并将它们压入操作数堆栈。 如果堆栈上有 >= 2 个操作数,则弹出 2,将它们与操作符堆栈中最顶部的操作符组合,并将此结构推回操作数树,除非 下一个运算符的优先级高于当前位于堆栈顶部的运算符。

这给我们留下了处理括号的问题。一个优雅的(我认为)解决方案是将每个运算符的优先级存储为变量中的数字。所以一开始,

int 加减 = 1; int mul, div = 2;

现在每次看到左括号时,所有这些变量都加 2,每次看到右括号时,所有变量都减 2。

这将确保 3*(4+5) 中的 + 比 * 具有更高的优先级,并且 3*4 不会被压入堆栈。相反,它会等待 5,推 4+5,然后推 3*(4+5)。

【讨论】:

【参考方案14】:

我已经使用一些基本技术编写了这样的解析器,例如 Infix -> RPN 和 Shunting Yard 和 Tree Traversals。 Here is the implementation I've came up with. 它是用 C++ 编写的,可以在 Linux 和 Windows 上编译。 欢迎提出任何建议/问题。

所以,让我们尝试通过所有三种方式来解决这个问题。你如何从算术表达式(例如在字符串中)例如“2 + (2)”到使用级联-if、函数指针表和/或多态性的表达式树?

这很有趣,但我认为这不属于面向对象编程的领域...我认为它与parsing techniques 有更多关系。

【讨论】:

【参考方案15】:

我有点把这个 c# 控制台应用程序放在一起作为概念证明。感觉它可能会好很多(GetNode 中的 switch 语句有点笨拙(因为我在尝试将类名映射到运算符时遇到了空白))。任何关于如何改进的建议都非常欢迎。

using System;

class Program

    static void Main(string[] args)
    
        string expression = "(((3.5 * 4.5) / (1 + 2)) + 5)";
        Console.WriteLine(string.Format("0 = 1", expression, new Expression.ExpressionTree(expression).Value));
        Console.WriteLine("\nShow's over folks, press a key to exit");
        Console.ReadKey(false);
    


namespace Expression

    // -------------------------------------------------------

    abstract class NodeBase
    
        public abstract double Value  get; 
    

    // -------------------------------------------------------

    class ValueNode : NodeBase
    
        public ValueNode(double value)
        
            _double = value;
        

        private double _double;
        public override double Value
        
            get
            
                return _double;
            
        
    

    // -------------------------------------------------------

    abstract class ExpressionNodeBase : NodeBase
    
        protected NodeBase GetNode(string expression)
        
            // Remove parenthesis
            expression = RemoveParenthesis(expression);

            // Is expression just a number?
            double value = 0;
            if (double.TryParse(expression, out value))
            
                return new ValueNode(value);
            
            else
            
                int pos = ParseExpression(expression);
                if (pos > 0)
                
                    string leftExpression = expression.Substring(0, pos - 1).Trim();
                    string rightExpression = expression.Substring(pos).Trim();

                    switch (expression.Substring(pos - 1, 1))
                    
                        case "+":
                            return new Add(leftExpression, rightExpression);
                        case "-":
                            return new Subtract(leftExpression, rightExpression);
                        case "*":
                            return new Multiply(leftExpression, rightExpression);
                        case "/":
                            return new Divide(leftExpression, rightExpression);
                        default:
                            throw new Exception("Unknown operator");
                    
                
                else
                
                    throw new Exception("Unable to parse expression");
                
            
        

        private string RemoveParenthesis(string expression)
        
            if (expression.Contains("("))
            
                expression = expression.Trim();

                int level = 0;
                int pos = 0;

                foreach (char token in expression.ToCharArray())
                
                    pos++;
                    switch (token)
                    
                        case '(':
                            level++;
                            break;
                        case ')':
                            level--;
                            break;
                    

                    if (level == 0)
                    
                        break;
                    
                

                if (level == 0 && pos == expression.Length)
                
                    expression = expression.Substring(1, expression.Length - 2);
                    expression = RemoveParenthesis(expression);
                
            
            return expression;
        

        private int ParseExpression(string expression)
        
            int winningLevel = 0;
            byte winningTokenWeight = 0;
            int winningPos = 0;

            int level = 0;
            int pos = 0;

            foreach (char token in expression.ToCharArray())
            
                pos++;

                switch (token)
                
                    case '(':
                        level++;
                        break;
                    case ')':
                        level--;
                        break;
                

                if (level <= winningLevel)
                
                    if (OperatorWeight(token) > winningTokenWeight)
                    
                        winningLevel = level;
                        winningTokenWeight = OperatorWeight(token);
                        winningPos = pos;
                    
                
            
            return winningPos;
        

        private byte OperatorWeight(char value)
        
            switch (value)
            
                case '+':
                case '-':
                    return 3;
                case '*':
                    return 2;
                case '/':
                    return 1;
                default:
                    return 0;
            
        
    

    // -------------------------------------------------------

    class ExpressionTree : ExpressionNodeBase
    
        protected NodeBase _rootNode;

        public ExpressionTree(string expression)
        
            _rootNode = GetNode(expression);
        

        public override double Value
        
            get
            
                return _rootNode.Value;
            
        
    

    // -------------------------------------------------------

    abstract class OperatorNodeBase : ExpressionNodeBase
    
        protected NodeBase _leftNode;
        protected NodeBase _rightNode;

        public OperatorNodeBase(string leftExpression, string rightExpression)
        
            _leftNode = GetNode(leftExpression);
            _rightNode = GetNode(rightExpression);

        
    

    // -------------------------------------------------------

    class Add : OperatorNodeBase
    
        public Add(string leftExpression, string rightExpression)
            : base(leftExpression, rightExpression)
        
        

        public override double Value
        
            get
            
                return _leftNode.Value + _rightNode.Value;
            
        
    

    // -------------------------------------------------------

    class Subtract : OperatorNodeBase
    
        public Subtract(string leftExpression, string rightExpression)
            : base(leftExpression, rightExpression)
        
        

        public override double Value
        
            get
            
                return _leftNode.Value - _rightNode.Value;
            
        
    

    // -------------------------------------------------------

    class Divide : OperatorNodeBase
    
        public Divide(string leftExpression, string rightExpression)
            : base(leftExpression, rightExpression)
        
        

        public override double Value
        
            get
            
                return _leftNode.Value / _rightNode.Value;
            
        
    

    // -------------------------------------------------------

    class Multiply : OperatorNodeBase
    
        public Multiply(string leftExpression, string rightExpression)
            : base(leftExpression, rightExpression)
        
        

        public override double Value
        
            get
            
                return _leftNode.Value * _rightNode.Value;
            
        
    

【讨论】:

很高兴看到一个完整的实现,但我有点困惑,为什么你将解析逻辑与表达式树表示结合起来,创建了解析逻辑与表达式树的紧密耦合。此外,您可以在令牌和它们映射到的类之间生成映射(由 xml、数据库、代码内字典或应用于表示节点的每个类的自定义属性驱动)。这里的问题是使用反射(通过 Activator 或其他一些技术)来生成你的类。 @Merritt mmm 好点子,下次我有空闲午餐时间时可能会再次破解。感谢您的建议。【参考方案16】:

好的,这是我的幼稚实现。抱歉,我不觉得为那个使用对象,但它很容易转换。我觉得有点像邪恶的威利(来自史蒂夫的故事)。

#!/usr/bin/env python

#tree structure [left argument, operator, right argument, priority level]
tree_root = [None, None, None, None]
#count of parethesis nesting
parenthesis_level = 0
#current node with empty right argument
current_node = tree_root

#indices in tree_root nodes Left, Operator, Right, PRiority
L, O, R, PR = 0, 1, 2, 3

#functions that realise operators
def sum(a, b):
    return a + b

def diff(a, b):
    return a - b

def mul(a, b):
    return a * b

def div(a, b):
    return a / b

#tree evaluator
def process_node(n):
    try:
        len(n)
    except TypeError:
        return n
    left = process_node(n[L])
    right = process_node(n[R])
    return n[O](left, right)

#mapping operators to relevant functions
o2f = '+': sum, '-': diff, '*': mul, '/': div, '(': None, ')': None

#converts token to a node in tree
def convert_token(t):
    global current_node, tree_root, parenthesis_level
    if t == '(':
        parenthesis_level += 2
        return
    if t == ')':
        parenthesis_level -= 2
        return
    try: #assumption that we have just an integer
        l = int(t)
    except (ValueError, TypeError):
        pass #if not, no problem
    else:
        if tree_root[L] is None: #if it is first number, put it on the left of root node
            tree_root[L] = l
        else: #put on the right of current_node
            current_node[R] = l
        return

    priority = (1 if t in '+-' else 2) + parenthesis_level

    #if tree_root does not have operator put it there
    if tree_root[O] is None and t in o2f:
            tree_root[O] = o2f[t]
            tree_root[PR] = priority
            return

    #if new node has less or equals priority, put it on the top of tree
    if tree_root[PR] >= priority:
        temp = [tree_root, o2f[t], None, priority]
        tree_root = current_node = temp
        return

    #starting from root search for a place with higher priority in hierarchy
    current_node = tree_root
    while type(current_node[R]) != type(1) and priority > current_node[R][PR]:
        current_node = current_node[R]
    #insert new node
    temp = [current_node[R], o2f[t], None, priority]
    current_node[R] = temp
    current_node = temp



def parse(e):
    token = ''
    for c in e:
        if c <= '9' and c >='0':
            token += c
            continue
        if c == ' ':
            if token != '':
                convert_token(token)
                token = ''
            continue
        if c in o2f:
            if token != '':
                convert_token(token)
            convert_token(c)
            token = ''
            continue
        print "Unrecognized character:", c
    if token != '':
        convert_token(token)


def main():
    parse('(((3 * 4) / (1 + 2)) + 5)')
    print tree_root
    print process_node(tree_root)

if __name__ == '__main__':
    main()

【讨论】:

以上是关于使用多态性的表达式评估和树行走? (阿拉史蒂夫耶格)的主要内容,如果未能解决你的问题,请参考以下文章

实现一个简单的解释器

值多态性和“产生异常”

PromiseKit 没有回调/解除分配? (阿拉莫菲尔)

iOS正则表达式阿拉伯语

营业执照编号 正则表达式

阿拉伯数字转换成中文大写,中文货币的表达方式 python