布尔代数化简复合条件判断,从数学原理到代码实现

1,743 阅读10分钟

最近做的一个分享,记录一下

引言

开发时写复合条件判断,由于业务变动,或者是逻辑不清,可能会写出很长的一段表达式,这样的代码阅读起来十分困难。

例如下面这段:

if (a && d || b && c && !d || (!a || !b) && c) {
  console.log('pass')
} else {
  console.log('fail')
}

你知道这段复合条件判断的逻辑含义吗?NO.

下面就给大家介绍下今天的主角,布尔代数。

布尔代数

在数学和数理逻辑中,逻辑代数(有时也称开关代数、布尔代数)是代数的一个分支,其变量的值仅为真和假两种真值(通常记作 1 和 0)。逻辑代数是乔治·布尔(George Boole)在他的第一本书《逻辑的数学分析》(1847年)中引入的,并在他的《思想规律的研究》(1854年)中更充分的提出了逻辑代数。

这段话是来自维基百科对布尔代数的介绍。

基本知识

布尔乘法/加法

在数学中,我们要到的最基本的运算法则是加、减、乘、除。同样,在布尔代数中,也存在着基本的运算法则,它们分别是AND(与)、OR(或)和NOT(非)。

布尔加法 =》OR(或)

布尔乘法 =〉AND(与)

布尔定律

  • 交换律
    • A+B = B+A
    • AB = BA
  • 结合律
    • A+(B+C) = (A+B)+C
    • (AB)C = A(BC)
  • 分配律
    • A(B+C) = AB+AC

布尔法则

列出12个基本法则,前9个可以很直观的看出来,后面三个可以推导得出

A、B或者C可以表示一个单变量,或者变量的组合

逻辑表达式

布尔表达式

布尔表达式(Boolean expression)是一段代码声明,它最终只有true(真)和false(假)两个取值。最简单的布尔表达式是等式(equality),这种布尔表达式用来测试一个值是否与另一个值相同。

由于狄摩根定理的存在,任何与运算都可以转换成或运算,任何或运算都可以转成与运算。任何布尔表达式都可以有两种标准形式。

根据真值表可以写出对应逻辑表达式的标准形式(最小项之和或最大项之积)

  • 就例如下图中x&&y的真值表,可以构造出 x&&y =>
    • xy 最小项之和
    • (!x+!y)(!x+y)(x+!y) 最大项之积

真值表

真值表是含n(n>=1)命题变项的命题公式 ,共有2^n组赋值将命题公式A在所有赋值之下取值的情况列成表

布尔代数变量的值仅为真和假两种真值(通常记作 1 和 0,1为真,0为假),利用这一特点,所以我们可以把所有的情况都列出来,形成一张表,这张表就是真值表

构造真值表通常遵循以下顺序:

  1. 找出表达式(命题公式)所有的逻辑变量,列出所有可能的赋值;
  2. 按从低到高的顺序写出各层次;
  3. 对应每个赋值,计算表达式各层次的值,直到最后计算出命题公式的值。

上图便是常见的与运算所对应的真值表,可见,对于_n_个变量的真值表,会有2^_n_种情况。

以上就是介绍化简之前,需要了解的一些基本概念啦,卡诺图等到下文中再聊。

常见的化简方法

简化的目的,就是为了得到用更少的逻辑运算来实现相同目的的表达式。常见的有三种方法。

  1. 公式法

前面已经介绍12个基本公式,在基本公式中,我们应当牢记一下几个常用结论

  • 1加任何变量,结果都是1;0乘任何变量,结果都是0.
  • 多个同一变量的和(或运算)仍然是它本身,例如:A+A+...+A = A
  • 多个同一变量的积(且运算)仍然是它本身,例如:A*A*...*A = A
  • 同一变量的原变量和反变量(非运算)之和恒为1
  • 同一变量的原变量和反变量之积恒为0
  • 狄摩根定律
  • 还需要记住下面这个公式

公式法是利用布尔代数的基本公式,对函数进行消项,消因子:

常用方法包括并项法、吸收法、消因子法、消项法、配项法

利用公式法,我们来化简分享开始时遇到的问题!

a && d || b && c && !d || (!a || !b) && c

  ad+bc!d+(!a+!b)c //狄摩根定律
= ad+bc!d+!(ab)c //利用最后一条公式,前两项中都存在d
= ad+bc!d+abc+!(ab)c
= ad+bc!d +(ab+!(ab))c
= ad+bc!d +c
= ad+c(b!d+1)
= ad +c


卡诺图化简

与公式法不同,卡诺图的化简更加直观,不需要记住复杂的公式,考虑各种化简方法。

基础知识

最小项:一个函数的某个乘积项包含了函数的全部变量,其中每个变量都以原变量或反变量的形式出现,且仅出现一次,则这个乘积项称为该函数的一个标准积项,通常称为最小项

最小项的表示方法:通常用mi来表示最小项。

下标i的确定方式:把最小项中原变量记为1,反变量记为0,当变量顺序确定后,可以按顺序排列成一个二进制数,则这个二进制数相对应十进制数,就是这个最小项的下标i

最小项的的相邻性:任何两个最小项如果他们只有一个因子不同其余因子都相同,则称这两个最小项为相邻最小项

相邻的两个最小项之和可以合并一项消去一个变量。如:

卡诺图怎么画

卡诺图的绘制相比较于公式法而言,显得不是很简洁,但是熟练掌握之后也能成为有效的化简手段,

以两个变量的进行举例

3变量的卡诺图

卡诺图相邻性的特点保证了几何相邻两方格所代表的最小项只有一个变量不同。因此,若相邻的方格都为1(简称1格)时,则对应的最小项就可以合并。合并的结果是消去这个不同的变量,只保留相同的变量。这是图形化简法的依据。

卡诺图画出来之后,最后一步就是画圈。

画圈原则

  1. 取大不取小,圈越大,消去的变量越多,与项越简单,能画入大圈就不画入小圈;
  2. 圈数越少,化简后的与项就越少;
  3. 一个最小项可以重复使用,即只要需要,一个方格可以同时被多圈所圈;
  4. 一个圈中的小方格至少有一个小方格不为其它圈所圈;
  5. 画圈必须覆盖完每一个填“1”方格为止。

最后画圈,一共两组

m1,m3,m5,m7,m13,m15,m9,m11 =>8个消去3个变量 c

m12,m13,m15,m14 =》4个消去2个变量 ad

所以最终结果是ad+c

Q-M法化简

不知道看完上面两种方法之后,对于逻辑表达式的化简是否有了一些概念。但是这两种方法都存在一个问题,那就是不方便使用机器语言来描述。

Q-M法概述

其基本原理是通过逐级合并相邻最小项并消去多余因子,其原理跟卡诺图化简法类似。只不过卡诺图用画圈的形式让化简更加直观通俗,而Q-M法的分组形式更利于程序化执行。

大致分为4步:

  1. 将布尔表达式的项按只含0个“1”,只含1个“1”,只含2个“1”,…,只含n个“1”(n为变量个数)划分为不同的Group,并按“1”的数量排列(升序或降序均可)成表;
  2. 准备一张新表。从含有最少数量的“1”的Group开始依次向下,将当前Group中的每一项与下一个Group的每一项比较。若两者只有一个变量不同,则将两项提取出来,并将不同的变量处用“-”标记,生成一个新的项。如果新的项在新表中已存在,则不执行动作;若不存在,则将这个新的项放到新表中的相应Group中。最后,在原表的两个Group中将提取的两项对应的“hasMerged”打上标记。
  3. 在新生成的表中,重复2,直到新表中不存在只有一个变量不同的项为止。
  4. 化简结果即为所有表中“hasMerged”未被标记的项的和。

我也简单按照这个思路用代码进行了功能实现

整体代码已经贴在文档末尾

化简功能代码实现思路

// 获取逻辑表达式中的所有变量
function getVariables(expression) {
  const reg = /[^\)\|\&\(!]+/g;
  const noSpaceString = expression.replace(/\s/g, "");
  return [...new Set(noSpaceString.match(reg))];
}

// 根据变量个数生成二进制数组

function getBinaryGroup(length) {
  // 所代表的值 2**length-1
  let binaryGroup = [];
  for (let i = 0; i < 2 ** length; i++) {
    binaryGroup.push(i.toString(2).padStart(length, "0").split(""));
  }
  return binaryGroup;
}

// 利用二进制数组对所有变量分别赋值,构造真值表
function evalPro(str) {
  var Fn = Function;
  return new Fn("return " + str)();
}

function inputToOutput(expression, variables, inputArr) {
  let outputResult = expression.replace(/\s/g, "");
  // todo 排除多次重复替换带来的错误结果 例如 ffalselse||false
  variables.map((item, index) => {
    const reg = new RegExp(item, "g");
    outputResult = outputResult.replace(
      reg,
      inputArr[index] === "1" ? "true" : "false"
    );
  });
  return evalPro(outputResult);
}

function getTruthTable(expression, variables, binaryGroup) {
  const truthList = [];
  binaryGroup.map((item, index) => {
    truthList[index] = inputToOutput(expression, variables, item);
  });
  return truthList;
  }

// 到这里才到Q-M法的第一步,将真值表内的值进行排序。计算逻辑变量中“1”的个数,按只含0个“1”,只含1个“1”,只含2个“1”,…,只含n个“1”(n为变量个数)划分为不同的Group,并按“1”的数量排列(升序或降序均可)成表;

function getFirstGroup(truthList, binaryGroup) {
  let group = [];
  for (let i = 0; i < binaryGroup[0].length + 1; i++) {
    group[i] = [];
  }
  truthList.map((item, index) => {
    if (item) {
      let temp = binaryGroup[index].filter((item, index) => {
        return item === "1";
      }).length;
      group[temp].push({
        hasIndex: [index],
        currentValue: binaryGroup[index],
        isMerged: false,
      });
    }
  });
  return group;
}



// 将表中每组的值进行合并,构造新表。如果表中的值可以继续合并,则重复此操作
// 判断两个数组是否可以合并
function canMerge(g1, g2) {
  // temp 用来计算不同值的数量 lastDifferentIndex记录最后一个不同值的index
  let temp = 0;
  let lastDifferentIndex = 0;
  let mergeGroup = JSON.parse(JSON.stringify(g1.currentValue));

  for (let i = 0; i < g1.currentValue.length; i++) {
    if (temp > 1) {
      return;
    }
    if (g2.currentValue[i] !== g1.currentValue[i]) {
      lastDifferentIndex = i;
      temp++;
    }
  }
  if (temp === 1) {
    mergeGroup[lastDifferentIndex] = "-";
    g1.isMerged = true;
    g2.isMerged = true;
    return {
      currentValue: mergeGroup,
      hasIndex: [...new Set([...g1.hasIndex, ...g2.hasIndex])],
      isMerged: false,
    };
  } else {
    return;
  }
}

function mergeGroup(groups = [], _noMergeGroup = []) {
  let nextGroup = [];
  for (let i = 0; i < groups.length; i++) {
    nextGroup[i] = [];
  }
  let noMergeGroup = JSON.parse(JSON.stringify(_noMergeGroup));
  for (let x = 0; x < groups.length - 1; x++) {
    for (let y = 0; y < groups[x].length; y++) {
      for (let i = x + 1; i < groups.length; i++) {
        for (let j = 0; j < groups[i].length; j++) {
          //  和 groups[i][j] 来进行合并
          const mergeResult = canMerge(groups[x][y], groups[i][j]);
          if (mergeResult) {
            // console.log(mergeResult, "mergeResult1");
            let temp = mergeResult.currentValue.filter((item, index) => {
              return item === "1";
            }).length;
            nextGroup[temp] ? null : (nextGroup[temp] = []);

            let hasIn =
              nextGroup[temp].filter((item) => {
                return (
                  item.currentValue.join() === mergeResult.currentValue.join()
                );
              }).length > 0
                ? true
                : false;
            if (!hasIn) {
              nextGroup[temp].push(mergeResult);
            }
          }
        }
      }
    }
  }
  const { noMerge, groupsLength } = findNoMergeGroup(groups);
  noMergeGroup = noMergeGroup.concat(noMerge);
  if (groupsLength === noMerge.length) {
    //即没有新增
    return noMergeGroup;
  } else {
    return mergeGroup(nextGroup, noMergeGroup);
  }
}

// 当所有的不可合并项都找到时,还需要对其中可能的重复项进行排除
function findUnqIndex(indexArr = []) {
  let temp = [];
  let result = [];
  indexArr.map((item) => {
    temp[item] === undefined ? (temp[item] = 1) : temp[item]++;
  });
  temp.map((item, index) => {
    if (item === 1) {
      result.push(index);
    }
  });
  return result;
}
function handleLoop(defaultArr, targetIndex, loopArr = []) {
  // console.log(loopArr, "loopArr");
  let currentLoopArr = JSON.parse(JSON.stringify(loopArr));
  loopArr.length === 0 &&
    defaultArr.map((item, index) => {
      currentLoopArr.push({
        hasTableIndex: item.index,
        hasDefaultIndex: [index],
      });
    });

  let isLoopOver = false; // 循环结束表者
  let nextLoopArr = []; // 下个循环体
  let loopResult = [];
  for (let i = 0; i < currentLoopArr.length; i++) {
    let maxIndex =
      currentLoopArr.length > 0
        ? Math.max(currentLoopArr[i].hasDefaultIndex)
        : 0;
    for (let j = maxIndex + 1; j < defaultArr.length; j++) {
      let hasTableIndex = [
        ...new Set(currentLoopArr[i].hasTableIndex.concat(defaultArr[j].index)),
      ];
      let hasDefaultIndex = JSON.parse(
        JSON.stringify(currentLoopArr[i].hasDefaultIndex)
      ).push(j);

      if (hasTableIndex.length === targetIndex.length) {
        loopResult.push({
          hasTableIndex,
          hasDefaultIndex,
        });
        isLoopOver = true;
      } else {
        nextLoopArr.push({
          hasTableIndex, // 这里的index是指真值表的index
          hasDefaultIndex, // 这里的index是指needMoreHandle中index
        });
      }
    }
  }
  if (isLoopOver) {
    return loopResult;
  } else {
    return handleLoop(defaultArr, targetIndex, nextLoopArr);
  }
}
function handleRepeat(needMoreHandle, targetIndex) {
  // targetIndex 合成目标
  let result = needMoreHandle.filter((item) => {
    return item.index.length === targetIndex.length;
  });

  let temp = [];
  if (result.length === 0) {
    const loopResult = handleLoop(needMoreHandle, targetIndex);
    loopResult.map((item) => {
      let oneAnswer = [];
      item.hasDefaultIndex.map((e) => {
        oneAnswer.push(e.value);
      });
      temp.push(oneAnswer);
    });
  } else {
    result.map((item) => {
      temp.push([item.value]);
    });
  }
  // console.log(result, targetIndex, temp, "res");
  return temp;
}

function arrToString(arr, variables) {
  let simplyArr = JSON.parse(JSON.stringify(arr));
  simplyArr.forEach((item, i) => {
    item.forEach((e, index) => {
      if (e === "1") {
        return (item[index] = variables[index]);
      } else if (e === "0") {
        return (item[index] = "!" + variables[index]);
      } else if (e === "-") {
        return (item[index] = "");
      }
    });
    const noEmpty = item.filter((e, index) => {
      return e !== "";
    });
    return (simplyArr[i] = noEmpty.join("&&"));
  });
  return simplyArr.join("||");
}

// 最后的处理,对可能存在的重复项进行排除
function finalHandle(group, variables) {
  let temp = [];
  let final = []; //最后的求解值
  let includedKeys = new Set(); // 所有已被选中行中,已经包含的列值   目的是为了减少needMoreHandle中index的处理
  let needMoreHandle = [];
  group.map((item) => {
    temp.push(item.hasIndex);
  });
  temp = temp.flat();
  const arrUnq = findUnqIndex(temp); // 找到所有独一无二的列对应的值
  for (let i = 0; i < group.length; i++) {
    let hasUnq = false; // 是否存在某个值占据独一无二的列
    for (let j = 0; j < group[i].hasIndex.length; j++) {
      if (arrUnq.includes(group[i].hasIndex[j])) {
        // 如果存在某个值占据独一无二的列,则去掉这行
        final.push(group[i].currentValue);
        group[i].hasIndex.map((item) => {
          includedKeys = includedKeys.add(item);
        });

        hasUnq = true;
        break;
      }
    }
    if (!hasUnq) {
      needMoreHandle.push(group[i]);
    }
  }
  let targetIndex = [];
  needMoreHandle.forEach((item, index) => {
    let indexArr = item.hasIndex.filter((item) => {
      return !includedKeys.has(item);
    });
    targetIndex = targetIndex.concat(indexArr);
    return (needMoreHandle[index] = {
      value: item.currentValue,
      index: indexArr,
    });
  });

  // 对needMoreHandle进行处理
  let finalString = arrToString(final, variables);
  // console.log(finalString, "finalSting");
  let extraArr = [];
  let finalStringArr = [];
  if (needMoreHandle.length > 0) {
    extraArr = handleRepeat(needMoreHandle, [...new Set(targetIndex)]);
    extraArr.map((item) => {
      finalStringArr.push(finalString + "||" + arrToString(item, variables));
    });
  } else {
    finalStringArr.push(finalString);
  }

  return finalStringArr;
}

总结

关于前两种化简方法的优缺点:

  • 公式法是利用逻辑代数的公式和规则(定理)来对逻辑函数化简,这种方法适用于各种复杂的逻辑函数,但需要熟练地运用公式和规则(定理),且具有一定的运用技巧。

  • 卡诺图化简法简单直观,容易掌握,但变量太多时卡诺图太复杂,一般说来变量个数大于等于5时该法已不适用。