使用多态性进行表达评估和树木行走? (ala Steve Yegge)

时间:2008-08-15 17:31:39

标签: oop recursion polymorphism binary-tree

今天早上,我正在阅读Steve Yegge's: When Polymorphism Fails,当我遇到一个问题时,他的一位同事在他们来亚马逊采访时曾经问过潜在的员工。

  

作为多态性的一个例子   动作,让我们来看看经典   “eval”面试问题,(作为   据我所知)被带到了亚马逊   作者:Ron Braunstein。问题是   这是一个非常丰富的,因为它设法   探究各种重要的   技能:OOP设计,递归,二进制   树,多态和运行时   打字,一般编码技巧,和(如果   你想让它变得更加困难)   解析理论。

     

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

     

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

     

问题的前半部分,哪个   一些人(我的名字   保护我垂死的气息,但他们的   首字母是威利刘易斯)的感觉是一个   工作要求如果你想打电话   你自己一个开发人员和工作   亚马逊,实际上有点难。该   问题是:你怎么去的?   算术表达式(例如在a   字符串)如“2 +(2)”到   表达树。我们可能有一个ADJ   在某些方面挑战这个问题   点。

     

下半场是:让我们说这是   一个2人的项目,和你的伴侣,   我们称之为“威利”的是谁   负责改造   字符串表达到树中。你得到   简单的部分:你需要决定什么   威利班是要建造的   树。你可以做任何事情   语言,但一定要选一个,   或威利会递给你大会   语言。如果他感觉很吵,那就是   将是一个没有的处理器   生产时间更长。

     

你会对有多少候选人感到惊讶   这个。

     

我不会放弃答案,而是答案   标准不良解决方案涉及使用   开关或案件陈述(或只是   良好的老式级联 - 如果)。一个   稍微好一点的解决方案涉及到   使用函数指针表,   和可能的最佳解决方案   涉及使用多态。一世   鼓励你完成它   一段时间。有趣的东西!

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

随意解决一个,两个或全部三个问题。

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

16 个答案:

答案 0 :(得分:11)

多态树行走,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)

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

程序结构:

抽象基类:节点

  • 所有节点都继承自此类

抽象基类:BinaryNode

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

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

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

数字等级:Num

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

答案 1 :(得分:4)

  

我认为问题在于我们需要解析perentheses,但它们不是二元运算符?我们应该将(2)作为单个标记,评估为2?

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

    +
   / \
  +   3
 / \
1   2

    +
   / \
  1   +
     / \
    2   3

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

答案 2 :(得分:4)

这进入了解析/编译器理论,这是一个兔子洞... The Dragon Book是编译器构造的标准文本,并将其置于极端。在这种特殊情况下,您希望为基本算术构造context-free grammar,然后使用该语法来解析abstract syntax tree。然后,您可以遍历树,从下往上减少它(此时您将应用多态/函数指针/ switch语句来减少树)。

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

答案 3 :(得分:4)

代表节点

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

  • 二进制节点:Add Minus Mul Div
    这些有两个孩子,左右两边

         +
        / \
    node   node
    
  • 保存值的节点:Val
    没有子节点,只是一个数值

  • 跟踪parens的节点:Paren为子表达式的单个子节点

        ( )
         |
        node
    

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

  • 节点
  • BinaryNode:继承自Node
  • Plus:继承自二进制节点
  • 减:继承自二进制节点
  • Mul:继承自Binary Node
  • Div:继承自Binary Node
  • 值:继承自Node
  • Paren:从节点继承

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

答案 4 :(得分:2)

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

答案 5 :(得分:2)

  

我不会放弃答案,而是答案   标准不良解决方案涉及使用   开关或案件陈述(或只是   良好的老式级联 - 如果)。一个   稍微好一点的解决方案涉及到   使用函数指针表,   和可能的最佳解决方案   涉及使用多态。

解释器的最后二十年演变可以被视为另一种方式 - 多态(例如天真的Smalltalk metacircular解释器)到函数指针(天真的lisp实现,线程代码,C ++)来切换(天真的字节码解释器),然后转到JIT等等 - 这需要非常大的类,或者(在单独的多态语言中)双重调度,这将多态性减少到类型情况,并且你又回到了第一阶段。在这里使用“最佳”的定义是什么?

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

答案 6 :(得分:2)

嗯...我不认为你可以在没有回溯的情况下为此编写一个自上而下的解析器,所以它必须是某种shift-reduce解析器。 LR(1)甚至LALR当然可以正常使用以下(ad-hoc)语言定义:

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

将其分成E1和E2是维持*和/ over +和 - 的优先级所必需的。

但是如果我不得不手工编写解析器,我就会这样做:

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

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

  • int plus,minus = 1;
  • int mul,div = 2;

现在每当你看到一个左括号将所有这些变量增加2时,每次看到右括号时,将所有变量减去2。

这将确保3 *(4 + 5)中的+优先于*,并且3 * 4不会被压入堆栈。相反,它将等待5,按4 + 5,然后按3 *(4 + 5)。

答案 7 :(得分:1)

回复:贾斯汀

我认为树看起来像这样:

  +
 / \
2  ( )
    |
    2

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

  +
 / \
2   2

在这种情况下,不需要parens,也不添加任何内容。他们不会在逻辑上添加任何东西,所以他们就会离开。

答案 8 :(得分:1)

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

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

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

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

答案 9 :(得分:0)

应该使用函数式语言imo。树很难用OO语言来表示和操纵。

答案 10 :(得分:0)

  

或许这是真正的问题:   你怎么能代表(2)作为BST?   那是绊倒我的那部分   起来。

递归。

答案 11 :(得分:0)

@Justin:

查看我关于代表节点的说明。如果您使用该方案,那么

2 + (2)

可以表示为

           .
          / \
         2  ( )
             |
             2

答案 12 :(得分:0)

正如人们之前提到过的,当你使用表达树时,没有必要使用parens。当您查看表达式树时,操作的顺序变得微不足道。 parens是解析器的提示。

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

如果您在字符串中允许浮点数,解析器也会显着更难。我不得不创建一个DFA来接受C中的浮点数 - 这是一项非常艰苦而详细的任务。请记住,有效的浮点数包括:10,10,10.123,9.876e-5,1.0f,.025等。我假设在面试中做了一些(有利于简洁和简洁)的特权。

答案 13 :(得分:0)

我用一些基本技术编写了这样的解析器 Infix -> RPNShunting YardTree TraversalsHere is the implementation I've came up with
它是用C ++编写的,可以在Linux和Windows上编译 欢迎提出任何建议/问题。

  

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

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

答案 14 :(得分:0)

我有点把这个c#console应用程序放在一起,作为一个概念证明。感觉它可能会好很多(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;
            }
        }
    }
}

答案 15 :(得分:0)

好的,这是我的天真实施。对不起,我觉得不能使用那个对象,但它很容易转换。我觉得有点像邪恶的威利(来自史蒂夫的故事)。

#!/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()