返回

JavaScript 真值表:位运算与语法分析器

javascript

生成真值表:JavaScript 方法与逻辑表达式求值

生成真值表是计算机科学中一项基础任务,它展现了逻辑表达式在各种输入组合下的输出结果。针对拥有三个变量 abc 的布尔表达式 !((a && b) || c),如何高效且准确地通过编程来生成其真值表并计算该表达式的值是本篇博客讨论的主题。

问题分析

代码示例中存在几个问题。其一,变量的真值计算部分不正确,循环内逻辑不能正确反映二进制位与变量值的对应关系。其二,代码试图使用字符串 (a&&b) 来做条件判断,这会导致结果始终为真,并未实际执行布尔逻辑运算。其三,逻辑表达式的求值,代码没有体现具体实现逻辑。

正确地生成真值表,首先需要明确每种变量组合的取值,然后根据布尔表达式进行运算。对于三个变量的情况,总共有 2^3 = 8 种可能的取值组合。接下来需要建立表达式解析和求值的机制。

解决方案 1:位运算与字符串替换

一种直接的方案是利用位运算生成所有可能的变量值组合,然后使用字符串替换的方式模拟逻辑运算。

实现步骤:

  1. 生成真值组合: 通过循环和位操作,产生 0 到 7 的整数,这些整数的二进制表示恰好对应变量 abc 的所有取值组合(0 代表 false,1 代表 true)。
  2. 构建环境: 根据生成的二进制值,构造 a, b, 和 c 的 true/false 值。
  3. 字符串替换与求值: 将表达式 !((a && b) || c) 中的 abc 替换成相应的 truefalse,并使用 eval 函数执行替换后的 JavaScript 代码,得到该组合下的结果。注意 eval 使用风险 ,后面会给出不使用 eval 的方案。
  4. 输出: 将结果记录成表格形式或其他方式。

代码示例:

function generateTruthTable(expression) {
  const variables = ['a', 'b', 'c'];
  const rows = [];
  for (let i = 0; i < 8; i++) {
    const values = {};
    variables.forEach((variable, index) => {
      values[variable] = Boolean(i & (1 << index)); // 位运算取值
    });

      let tempExp = expression
          .replace(/a/g, values.a)
          .replace(/b/g, values.b)
          .replace(/c/g, values.c);
      let result = eval(tempExp);


    rows.push({
      a: values.a,
      b: values.b,
      c: values.c,
      result: result,
    });
  }
  return rows;
}

const expression = '!(a && b || c)';
const table = generateTruthTable(expression);

console.log("  a    b    c  |  result");
table.forEach(row => {
  console.log(`${row.a} ${row.b} ${row.c}  |  ${row.result}`);
});

说明:
此代码使用位运算来生成每一种变量组合,i & (1 << index) 实现了提取指定位置二进制位的值。同时 eval 函数承担表达式求值。

注意:
使用 eval 函数需要特别注意其带来的安全风险,例如表达式字符串是从用户输入的。eval 会执行字符串里面的 JavaScript 代码,会产生严重的安全隐患。接下来介绍的方案 2 将给出更加安全、可靠的实现方式。

解决方案 2:递归下降语法分析器

为规避 eval 带来的安全问题,可以实现一个简单的递归下降语法分析器。这种方法可以将布尔表达式解析为抽象语法树,然后通过遍历语法树来计算结果,有效地避免了字符串注入的风险。

实现步骤:

  1. 词法分析(Tokenize): 将布尔表达式的字符串拆分成词素(token),如变量、操作符、括号等。
  2. 语法分析(Parse): 使用递归下降法将词素转换为抽象语法树(AST)。AST 明确表达式中各组成部分(如变量、逻辑运算符)之间的关系。
  3. 求值: 遍历 AST,结合当前变量取值,递归地计算布尔表达式的结果。

代码示例:

class Tokenizer {
    constructor(source) {
        this.source = source;
        this.index = 0;
    }

    peek() {
        return this.source[this.index];
    }

    next() {
        return this.source[this.index++];
    }
    isAtEnd(){
      return this.index >= this.source.length;
    }
}

class Parser {
    constructor(tokens) {
        this.tokens = tokens;
        this.index = 0;
    }
    peek() {
        return this.tokens[this.index];
    }
    next(){
        return this.tokens[this.index++]
    }
    isAtEnd(){
        return this.index >= this.tokens.length
    }
    
    parse(){
      return this.expression();
    }


    expression(){
      return this.or();
    }

    or(){
      let left = this.and();

      while(this.peek() ==='||'){
         this.next();
         const right = this.and();
         left ={op:'||',left: left,right: right};
      }

      return left;
    }
    and(){
        let left = this.not();
        while(this.peek() === '&&'){
            this.next();
          const right = this.not();
           left ={op:'&&',left: left,right: right}
        }
      return left
    }
    not(){

        if(this.peek() === '!'){
           this.next();
            const right = this.primary();
           return {op: '!', right}
        }

      return this.primary();
    }
    primary() {
        let current = this.peek();
         if(current === '(') {
             this.next();
              const exp = this.expression();
             if(this.peek()!==')'){
                throw "missing close paren"
              }
              this.next()
              return exp;
           }

        if (['a', 'b', 'c'].includes(current)) {
             this.next();
              return {value: current};
        }

    }


}


function evaluate(node, env){
    if(node.value){
        return env[node.value];
    }

    if(node.op==='!'){
      return ! evaluate(node.right, env);
    }

    const left = evaluate(node.left,env);
    const right = evaluate(node.right, env);


    switch (node.op){
        case '&&':
            return left && right
        case '||':
            return left || right;
    }

}

function tokenize(source) {
  const tokenizer = new Tokenizer(source);
  const tokens = [];
  while(!tokenizer.isAtEnd()) {
       const c = tokenizer.peek();
        if (['(', ')', '!', '&', '|', 'a', 'b', 'c'].includes(c)) {
             tokens.push(tokenizer.next())
          } else if(c ===' '){
            tokenizer.next() //ignore white space
          }
      else{
           throw "invalid token: " + c
          }
  }
    return tokens
}


function generateTruthTableAdvanced(expression) {
  const variables = ['a', 'b', 'c'];
    const rows = [];

     const tokens = tokenize(expression)
     const parser = new Parser(tokens);
     const ast = parser.parse();


  for (let i = 0; i < 8; i++) {
    const values = {};
    variables.forEach((variable, index) => {
      values[variable] = Boolean(i & (1 << index)); //位运算生成
    });
    const result = evaluate(ast,values)


    rows.push({
      a: values.a,
      b: values.b,
      c: values.c,
      result: result,
    });
  }
    return rows;

}
const expressionAdv = '!(a && b || c)';
const tableAdv = generateTruthTableAdvanced(expressionAdv);


console.log("\n   a      b     c   |    result(Adv)");
tableAdv.forEach(row => {
    console.log(`${row.a}  ${row.b}   ${row.c}   |  ${row.result}`);
  });


说明:

此方案提供了 TokenizerParser 类,分别实现词法分析和语法分析。代码构建抽象语法树(AST), 并利用递归方法求值。 evalute 函数利用提供的 env 参数计算表达式的值。这种实现方式完全不依赖于 eval 函数。提高了表达式处理的安全性。

结论

两种方法都可以有效地解决 “生成真值表” 的问题。 简单替换结合 eval 速度快,代码易于理解,但在处理外部表达式时要警惕安全问题; 递归下降分析器避免了安全风险,提供了一个更为通用的解决方案,同时对表达式的扩展和优化提供了更大的空间。