14

我有以下BoolExpr课程:

class BoolExpr
{
    public enum BOP { LEAF, AND, OR, NOT };

    //
    //  inner state
    //

    private BOP    _op;
    private BoolExpr _left;
    private BoolExpr _right;
    private String   _lit;

    //
    //  private constructor
    //

    private BoolExpr(BOP op, BoolExpr left, BoolExpr right)
    {
        _op = op;
        _left  = left;
        _right = right;
        _lit = null;
    }

    private BoolExpr(String literal)
    {
        _op = BOP.LEAF;
        _left  = null;
        _right = null;
        _lit = literal;
    }

    //
    //  accessor
    //

    public BOP Op
    {
        get { return _op;  }
        set { _op = value; }
    }

    public BoolExpr Left
    {
        get { return _left;  }
        set { _left = value; }
    }

    public BoolExpr Right
    {
        get { return _right;  }
        set { _right = value; }
    }

    public String Lit
    {
        get { return _lit; }
        set { _lit = value; }
    }

    //
    //  public factory
    //

    public static BoolExpr CreateAnd(BoolExpr left, BoolExpr right)
    {
        return new BoolExpr(BOP.AND, left, right);
    }

    public static BoolExpr CreateNot(BoolExpr child)
    {
        return new BoolExpr(BOP.NOT, child, null);
    }

    public static BoolExpr CreateOr(BoolExpr left, BoolExpr right)
    {
        return new BoolExpr(BOP.OR, left, right);
    }

    public static BoolExpr CreateBoolVar(String str)
    {
        return new BoolExpr(str);
    }

    public BoolExpr(BoolExpr other)
    {
        // No share any object on purpose
        _op = other._op;
        _left  = other._left  == null ? null : new BoolExpr(other._left);
        _right = other._right == null ? null : new BoolExpr(other._right);
        _lit = new StringBuilder(other._lit).ToString();
    }

    //
    //  state checker
    //

    Boolean IsLeaf()
    {
        return (_op == BOP.LEAF);
    }

    Boolean IsAtomic()
    {
        return (IsLeaf() || (_op == BOP.NOT && _left.IsLeaf()));
    }
}

我应该使用什么算法来解析像“”这样的输入布尔表达式字符串¬((A ∧ B) ∨ C ∨ D)并将其加载到上面的类中?

4

2 回答 2

51

TL;DR:如果您想查看代码,请跳至答案的第二部分。

我会从表达式构建一棵树以进行解析,然后首先深度遍历它。您可以参考有关二叉表达式树的维基百科文章,以了解我的建议。

  1. 首先添加省略的可选括号以使下一步更容易
  2. 当您读取任何不是运算符或括号的内容时,请创建一个 LEAF 类型节点
  3. 当您读取任何运算符(在您的情况下not为 , and, or)时,创建相应的运算符节点
  4. 二元运算符将前面和后面的节点作为子节点,一元运算符只获取下一个。

因此,对于您的示例¬((A ∧ B) ∨ C ∨ D),算法将如下所示:

¬((A ∧ B) ∨ C ∨ D)变成¬(((A ∧ B) ∨ C) ∨ D)

  1. 创建一个NOT节点,它将获得以下作为子节点打开的结果。
  2. 创建A LEAF节点,AND节点和B LEAF节点。 ANDAB作为孩子。
  3. 创建OR节点,它具有先前创建AND的子LEAF节点和C.
  4. 创建OR节点,它具有先前创建OR的和作为子节点的新节点D

此时,您的树如下所示:

  NOT
   |
  OR
  /\
 OR D
 / \
AND C
/\
A B

然后,您可以添加一个 Node.Evaluate() 方法,该方法基于其类型进行递归评估(此处可以使用多态性)。例如,它可能看起来像这样:

class LeafEx {
    bool Evaluate() {
        return Boolean.Parse(this.Lit);
    }
}

class NotEx {
    bool Evaluate() {
        return !Left.Evaluate();
    }
}

class OrEx {
    bool Evaluate() {
        return Left.Evaluate() || Right.Evaluate();
    }
}

等等等等。要获得表达式的结果,您只需调用

bool result = Root.Evaluate();

好吧,既然这不是一项任务,而且它实际上是一件有趣的事情,我就继续了。我将在此处发布的某些代码与我之前描述的内容无关(并且缺少某些部分),但我将在答案中保留顶部以供参考(其中没有任何问题(希望如此!))。

请记住,这远非最佳,我努力不修改您提供的 BoolExpr 类。修改它可以让您减少代码量。也根本没有错误检查。

下面是主要方法

static void Main(string[] args)
{
    //We'll use ! for not, & for and, | for or and remove whitespace
    string expr = @"!((A&B)|C|D)";
    List<Token> tokens = new List<Token>();
    StringReader reader = new StringReader(expr);

    //Tokenize the expression
    Token t = null;
    do
    {
        t = new Token(reader);
        tokens.Add(t);
    } while (t.type != Token.TokenType.EXPR_END);

    //Use a minimal version of the Shunting Yard algorithm to transform the token list to polish notation
    List<Token> polishNotation = TransformToPolishNotation(tokens);

    var enumerator = polishNotation.GetEnumerator();
    enumerator.MoveNext();
    BoolExpr root = Make(ref enumerator);

    //Request boolean values for all literal operands
    foreach (Token tok in polishNotation.Where(token => token.type == Token.TokenType.LITERAL))
    {
        Console.Write("Enter boolean value for {0}: ", tok.value);
        string line = Console.ReadLine();
        booleanValues[tok.value] = Boolean.Parse(line);
        Console.WriteLine();
    }

    //Eval the expression tree
    Console.WriteLine("Eval: {0}", Eval(root));

    Console.ReadLine();
}

标记化阶段为表达式的所有标记创建一个 Token 对象。它有助于将解析与实际算法分开。这是执行此操作的 Token 类:

class Token
{
    static Dictionary<char, KeyValuePair<TokenType, string>> dict = new Dictionary<char, KeyValuePair<TokenType, string>>()
    {
        {
            '(', new KeyValuePair<TokenType, string>(TokenType.OPEN_PAREN, "(")
        },
        {
            ')', new KeyValuePair<TokenType, string>(TokenType.CLOSE_PAREN, ")")
        },
        {
            '!', new KeyValuePair<TokenType, string>(TokenType.UNARY_OP, "NOT")
        },
        {
            '&', new KeyValuePair<TokenType, string>(TokenType.BINARY_OP, "AND")
        },
        {
            '|', new KeyValuePair<TokenType, string>(TokenType.BINARY_OP, "OR")
        }
    };

    public enum TokenType
    {
        OPEN_PAREN,
        CLOSE_PAREN,
        UNARY_OP,
        BINARY_OP,
        LITERAL,
        EXPR_END
    }

    public TokenType type;
    public string value;

    public Token(StringReader s)
    {
        int c = s.Read();
        if (c == -1)
        {
            type = TokenType.EXPR_END;
            value = "";
            return;
        }

        char ch = (char)c;

        if (dict.ContainsKey(ch))
        {
            type = dict[ch].Key;
            value = dict[ch].Value;
        }
        else
        {
            string str = "";
            str += ch;
            while (s.Peek() != -1 && !dict.ContainsKey((char)s.Peek()))
            {
                str += (char)s.Read();
            }
            type = TokenType.LITERAL;
            value = str;
        }
    }
}

此时,在 main 方法中,您可以看到我按照波兰表示法顺序转换了标记列表。它使树的创建变得更加容易,为此我使用了Shutting Yard Algorithm的修改实现:

static List<Token> TransformToPolishNotation(List<Token> infixTokenList)
{
    Queue<Token> outputQueue = new Queue<Token>();
    Stack<Token> stack = new Stack<Token>();

    int index = 0;
    while (infixTokenList.Count > index)
    {
        Token t = infixTokenList[index];

        switch (t.type)
        {
            case Token.TokenType.LITERAL:
                outputQueue.Enqueue(t);
                break;
            case Token.TokenType.BINARY_OP:
            case Token.TokenType.UNARY_OP:
            case Token.TokenType.OPEN_PAREN:
                stack.Push(t);
                break;
            case Token.TokenType.CLOSE_PAREN:
                while (stack.Peek().type != Token.TokenType.OPEN_PAREN)
                {
                    outputQueue.Enqueue(stack.Pop());
                }
                stack.Pop();
                if (stack.Count > 0 && stack.Peek().type == Token.TokenType.UNARY_OP)
                {
                    outputQueue.Enqueue(stack.Pop());
                }
                break;
            default:
                break;
        }

        ++index;
    }
    while (stack.Count > 0)
    {
        outputQueue.Enqueue(stack.Pop());
    }

    return outputQueue.Reverse().ToList();
}

经过这种转换,我们的令牌列表变为NOT, OR, OR, C, D, AND, A, B

此时,我们已准备好创建表达式树。波兰表示法的属性允许我们遍历令牌列表并递归创建树节点(我们将使用您的BoolExpr类):

static BoolExpr Make(ref List<Token>.Enumerator polishNotationTokensEnumerator)
{
    if (polishNotationTokensEnumerator.Current.type == Token.TokenType.LITERAL)
    {
        BoolExpr lit = BoolExpr.CreateBoolVar(polishNotationTokensEnumerator.Current.value);
        polishNotationTokensEnumerator.MoveNext();
        return lit;
    }
    else
    {
        if (polishNotationTokensEnumerator.Current.value == "NOT")
        {
            polishNotationTokensEnumerator.MoveNext();
            BoolExpr operand = Make(ref polishNotationTokensEnumerator);
            return BoolExpr.CreateNot(operand);
        }
        else if (polishNotationTokensEnumerator.Current.value == "AND")
        {
            polishNotationTokensEnumerator.MoveNext();
            BoolExpr left = Make(ref polishNotationTokensEnumerator);
            BoolExpr right = Make(ref polishNotationTokensEnumerator);
            return BoolExpr.CreateAnd(left, right);
        }
        else if (polishNotationTokensEnumerator.Current.value == "OR")
        {
            polishNotationTokensEnumerator.MoveNext();
            BoolExpr left = Make(ref polishNotationTokensEnumerator);
            BoolExpr right = Make(ref polishNotationTokensEnumerator);
            return BoolExpr.CreateOr(left, right);
        }
    }
    return null;
}

现在我们是金子了!我们有表示表达式的表达式树,因此我们将询问用户每个文字操作数的实际布尔值并评估根节点(这将根据需要递归地评估树的其余部分)。

下面是我的 Eval 函数,请记住,如果我修改了你的BoolExpr类,我会使用一些多态性来使这个更干净。

static bool Eval(BoolExpr expr)
{
    if (expr.IsLeaf())
    {
        return booleanValues[expr.Lit];
    }

    if (expr.Op == BoolExpr.BOP.NOT)
    {
        return !Eval(expr.Left);
    }

    if (expr.Op == BoolExpr.BOP.OR)
    {
        return Eval(expr.Left) || Eval(expr.Right);
    }

    if (expr.Op == BoolExpr.BOP.AND)
    {
        return Eval(expr.Left) && Eval(expr.Right);
    }

    throw new ArgumentException();
}

正如预期的那样,为我们的测试表达式分别输入¬((A ∧ B) ∨ C ∨ D)值会产生结果。false, true, false, trueA, B, C, Dfalse

于 2013-07-10T13:55:39.067 回答
3

从算法的角度来看,要解析一个表达式,您需要一个堆栈。

我们使用两步算法:

  1. 乐行

词法分析的目的是获取 'keywords'、'identifiers' 和 'separators' : - 关键字是 'if' 'then' 'else' '(' ')' '/\' '/' etc... -在您的情况下,标识符是“A”、“B”、“C”等... - 分隔符是空格、制表符、行尾、文件结尾等...

词法分析包括使用自动机。在词法分析中,您将逐个字符地读取输入字符串。当您遇到一个与您的关键字、标识符、分隔符中的一个兼容的字符时,您会开始一个字符序列。当您遇到分隔符时,您会停止序列,请在序列的字典中查找关键字(如果不是,则为标识符);然后将元组[序列,关键字或标识符/类]放在堆栈上。

我把小关键字 '(' 的情况留给你作为练习,它也可以看作是分隔符。

  1. 解析

解析类似于语法。在您的情况下,唯一要检查的规则是逗号和二进制操作,并且只是一个简单的标识符。

正式地:

expression::
  '(' expression ')'
  expression /\ expression
  expression \/ expression
  identifier

这可以通过递归函数来编写。首先反转你的堆栈,然后:

myParseExpression(stack, myC#ResultObject)
{
    if(stack.top = kewyord.'('  )
         then myParseOpenComma(all stack but top, myC#ResultObject)
    if(stack.top = keyword.'/\')
         then myParseBinaryAnd(stack, myC#ResultObject)
}

myParseOpenComma(stack, myC#ResultObject)
{
 ...
}

myParseBinaryAnd(stack, myC#ResultObject)
{
 myNewRigthPartOfExpr = new C#ResultObject
 myParseExpression(stack.top, myNewRigthPartOfExpr)
 remove top of stack;
 myNewLeftPartOfExpr = new C#ResultObject
 myParseExpression(stack.top, myNewLeftPartOfExpr)

 C#ResultObject.add("AND", myNewRigthPartOfExpr, myNewLeftPartOfExpr)
}

...

有多个函数可以相互共享递归。作为练习,尝试添加否定。

  • 词法分析传统上由词法分析器(如 lex 工具)完成。
  • 解析传统上由解析器(如野牛工具)完成。
  • 工具允许编写这些函数,就像我在正式表达式中所做的那样。

这些方面是程序编译的基础。对这些东西进行编码会大大提高你的水平,因为它既困难又基础。

于 2013-07-10T13:29:53.867 回答