简介重言式判别是数据结构课程设计中的常见题目这套资料面向计算机相关专业本科生完整呈现从问题分析到代码实现的课程设计过程。压缩包共4个文件包含2个C语言源程序和2份Word设计文档整体体积约38KB结构清晰便于查阅。源程序以二叉树存储布尔表达式叶子节点表示变量或常量非叶子节点为与、或、非等逻辑运算采用后序遍历计算子树结果并用栈暂存中间值最终判定表达式是否为重言式文档部分则详细记录了设计思路、二叉树构建与遍历算法、栈的运用、测试用例以及界面交互方案便于直接用作答辩材料。目前该资源在CSDN已有380人学习适合正在完成此类课程设计、需要源码与文档范例的同学对照参考也可为后续拓展为图形界面或自动生成真值表提供基础。1. 重言式判别程序一门让你把离散数学算到机器里的必修课期末课程设计清单下来最容易被低估的就是这个“重言式判别程序”。名字听起来简单只是判断一个命题逻辑公式是否恒真——但一旦要求从命令行输入一串公式例如((p-q)(q-r))-(p-r)程序输出“该公式是重言式”你就得同时处理表达式解析、运算符优先级、括号配对、变量去重和 2^n 次真值遍历。这门课设恰好卡在离散数学、数据结构和编译原理的交界处做一遍等于把三门课最抽象的部分各自练了一手。适合离散数学刚结课、想把逻辑公式从纸面搬到命令行的同学也适合拿它当小型语法分析器练手的初学者。2. 判别算法怎么选真值表法、归谬法与主范式的取舍拿到题目先别急着写代码第一步是选判定策略。电脑没有人脑那么擅长逻辑推演它最擅长的是枚举。所谓重言式就是“所有赋值组合下取值都为真”的公式那最朴素的做法就是把所有赋值组合全部试一遍这就是真值表法。这个方案在课程设计里几乎是最稳的选择原因有四条实现直观、结果可演示、错误可定位、代码量和难度都在课程设计合理范围内。归谬法和主范式法听起来更“高级”但它们是给自动化定理证明或者 SAT 求解器准备的思路。归谬法需要你先否定整个公式再判断变换后的公式是不是不可满足的这一步本质上是把问题转化为 SAT 问题而处理 SAT 问题要写 DPLL 或 CDCL 这类算法主范式法需要把公式等价变换成合取范式或析取范式变换过程本身就容易出 bug而且一旦 bug 出现在中间变换里你几乎没法通过真值表去做人工对照。所以我的建议很直接课程设计以真值表法为主线做足做透归谬法和主范式放到课程设计报告的“拓展讨论”章节里写两段原理和伪代码作为知识面的加分项但别把它们写进核心实现。2.1 真值表法为什么是课程设计的第一选择判断一个命题公式是不是重言式本质上是在问对所有命题变元的赋值组合公式的取值是否恒为真。真值表法的核心就是把变元的所有赋值组合列成一张表逐行代入公式求值。做课程设计时真值表是天然的答案依据老师要看的不是程序冷冰冰地输出一句“是重言式”而是要看程序给出完整的真值表从第一行到最后一行逐步验证。另一个关键理由是这种算法每一步都很透明结果可人工核对。命题逻辑里判别重言式的教科书算法不止一种但只有真值表每一步都可以用笔和纸跨查。哪怕程序运行结果有误把真值表逐行打印到控制台对着课本上手动列出的真值表就能定位到是哪一行赋值出错。对于一个以“完成并演示”为目标的课程设计这种可调试性远比其他算法重要。我一般会在工程里定这样一套流程先解析表达式生成语法树再收集全部命题变元然后用位运算生成 2^n 行赋值逐行求值并统计真值最后按统计结果输出“重言式 / 矛盾式 / 可满足式”。四个步骤彼此独立每一步都可以单独写测试用例出错时一眼就能看出是解析层的问题还是求值层的问题。2.2 归谬法和主范式法知道原理别在代码里硬刚归谬法的出发点是“如果公式是重言式那它的否定就不可满足”。所以流程是先构造 ¬F然后设法证明 ¬F 是矛盾式。听起来简单但判断一个命题逻辑公式是否不可满足本质上就是一个 SAT 判定问题。如果你去写一个完整的 DPLL 求解器分支启发式、单子句传播、冲突子句学习这些机制一个都不能少工作量比真值表法大一个数量级而且对课程设计来说完全杀鸡用牛刀。主范式法则是先把公式化成合取范式或析取范式。如果一个公式是重言式它的合取范式里的每个子句都要包含至少一对互补文字如果是矛盾式它的析取范式同样有这类特征。但范式化过程本身是一连串等价变换每一步都依赖分配律、德摩根律和双重否定消去变换链条越长出错的可能性就越高。最关键的是这类中间过程很难像真值表那样直观展示给老师看演示效果反而不如一张完整真值表有说服力。所以我在课程设计报告里会这样处理正文实现完整真值表法附录里给出归谬法的伪代码和公式推导说明两者等价性并对比三者在 n 个变元场景下的时间复杂度和空间复杂度。这样既展示了知识深度又避开了实现风险。2.3 复杂度边界多少命题变元还能扛得住真值表法的时间复杂度是 O(2^n × m)其中 n 是命题变元个数m 是语法树节点数。每加一个变元行数就翻一倍。下面是实测中常见的量级参考命题变元数 n真值表行数 2^n单行求值开销工程表现532微秒级闪电完成101024微秒级肉眼无感1532768毫秒级正常输出201048576毫秒级约 1 秒内完成2533554432毫秒级有明显等待几十秒级别做课程设计演示n 在 10 到 20 之间都是舒适区。n 超过 20 之后输出真值表本身就会刷屏这时可以加一个开关超出某个行数阈值只输出判定结论不再打印完整表格。如果你的题目要求支持更多变元那就不再适合真值表法可以转向 BDD二叉决策图或者增量式 SAT 求解。但课程设计层面坚持 2^n 穷举并做好行数限制提示已经能拿到很好的完成度。3. 从字符串到语法树表达式解析是成败的第一道关重言式判别程序的第一步不是判别而是把用户输入的字符串变成程序能理解的结构。你要面对的是像((p-q)(q-r))-(p-r)这样的原始输入里面有括号、连字符、大于号、小写字母和可能存在的空格。逐字符扫描直接判断会很痛苦常见做法是分成两段走先做词法分析把字符串拆成 Token 流再做语法分析把 Token 流组装成语法树。词法分析解决的是“识别单词”语法分析解决的是“识别结构”。这在编译原理里是两回事在课程设计里也必须分两层写否则后面每加一个运算符都要改一堆索引逻辑改到你自己都绕不清。3.1 词法分析把(pq)-r拆成 Token 流Token 是词法分析的最小单位。对于这个程序Token 一共就几类命题变元、真实、假、五个逻辑运算符、左右括号、以及表达式结束符。先定义枚举和 Token 结构// TokenType.java enum TokenType { VAR, TRUE, FALSE, NOT, AND, OR, IMPLY, IFF, LPAREN, RPAREN, END } // Token.java class Token { TokenType type; String text; // 变量名或符号原文 int position; // 在原始表达式中的位置 Token(TokenType type, String text, int position) { this.type type; this.text text; this.position position; } }接下来的词法分析器逐字符扫描把字符归类成 Token。这里的关键坑在于-是两个字符组成的运算符-是三个字符扫描时必须先做复合符号匹配再退回到单字符匹配// Lexer.java class Lexer { private final String src; private int pos 0; Lexer(String src) { this.src src; } Token next() { while (pos src.length() Character.isWhitespace(src.charAt(pos))) { pos; } if (pos src.length()) { return new Token(TokenType.END, , pos); } char c src.charAt(pos); if (Character.isLetter(c)) { int start pos; while (pos src.length() (Character.isLetterOrDigit(src.charAt(pos)) || src.charAt(pos) _)) { pos; } String word src.substring(start, pos); if (word.equals(true)) return new Token(TokenType.TRUE, word, start); if (word.equals(false)) return new Token(TokenType.FALSE, word, start); return new Token(TokenType.VAR, word.toLowerCase(), start); // 统一转小写 } if (c - pos 1 src.length() src.charAt(pos 1) ) { Token t new Token(TokenType.IMPLY, -, pos); pos 2; return t; } if (c pos 2 src.length() src.charAt(pos 1) - src.charAt(pos 2) ) { Token t new Token(TokenType.IFF, -, pos); pos 3; return t; } switch (c) { case !: return new Token(TokenType.NOT, !, pos); case : return new Token(TokenType.AND, , pos); case |: return new Token(TokenType.OR, |, pos); case (: return new Token(TokenType.LPAREN, (, pos); case ): return new Token(TokenType.RPAREN, ), pos); default: throw new ParseException(位置 pos 无法识别的字符 c ); } } }这段代码有个点需要特别说明变量名统一转成小写。命题逻辑里 p 和 P 在语义上是同一个变元如果大小写不统一后面做变量收集时 p 和 P 会被当成两个变元真值表行数直接翻倍。这一点是我自己踩过的坑后面避坑章节还会再提一次。另一个细节是位置记录。每个 Token 记录 position这样一旦抛出 ParseException你能精确告诉用户“第 7 个字符有问题”而不是给一句含糊的“输入错误”。这对排错体验非常重要也是课程设计评分时一个隐形的加分点。3.2 递归下降用五层文法吃掉优先级词法分析解决了“读字符”接下来解决“读结构”。逻辑运算符的优先级从低到高依次是等价-、蕴含-、或|、与、非!括号优先级最高。要把这个优先级顺序吃进去最直观的写法是递归下降一层文法对应一个方法// Parser.java class Parser { private final Lexer lexer; private Token lookahead; Parser(Lexer lexer) { this.lexer lexer; this.lookahead lexer.next(); } private void consume() { lookahead lexer.next(); } ExprNode parse() { ExprNode node parseIff(); if (lookahead.type ! TokenType.END) { throw new ParseException(表达式末尾有多余内容: lookahead.text); } return node; } private ExprNode parseIff() { ExprNode left parseImply(); while (lookahead.type TokenType.IFF) { consume(); ExprNode right parseImply(); left new IffNode(left, right); } return left; } private ExprNode parseImply() { ExprNode left parseOr(); while (lookahead.type TokenType.IMPLY) { consume(); ExprNode right parseOr(); left new ImplyNode(left, right); // 语义等价于 !left || right } return left; } private ExprNode parseOr() { ExprNode left parseAnd(); while (lookahead.type TokenType.OR) { consume(); ExprNode right parseAnd(); left new OrNode(left, right); } return left; } private ExprNode parseAnd() { ExprNode left parseNot(); while (lookahead.type TokenType.AND) { consume(); ExprNode right parseNot(); left new AndNode(left, right); } return left; } private ExprNode parseNot() { if (lookahead.type TokenType.NOT) { consume(); return new NotNode(parseNot()); // 支持连续否定 !!p } return parsePrimary(); } private ExprNode parsePrimary() { if (lookahead.type TokenType.VAR) { String name lookahead.text; consume(); return new VarNode(name); } if (lookahead.type TokenType.TRUE) { consume(); return new ConstNode(true); } if (lookahead.type TokenType.FALSE) { consume(); return new ConstNode(false); } if (lookahead.type TokenType.LPAREN) { consume(); ExprNode inner parseIff(); if (lookahead.type ! TokenType.RPAREN) { throw new ParseException(缺少右括号); } consume(); return inner; } throw new ParseException(意外的 token: lookahead.text); } }递归下降的核心逻辑是每个方法负责解析一层优先级然后调用下一层方法处理更高优先级的子表达式。比如 parseOr 看到的是|它会先调 parseAnd 把两侧的表达式解析完再处理|本身。这样p|qr解析出来的结果是p | (q r)因为 parseAnd 比 parseOr 调用得更深。这段代码还有一个设计点适合演示parseImply 里用while而不是if。这意味着写法完全支持多级蕴含例如p-q-r会解析成(p-q)-r左结合和大多数教材的约定一致。如果你希望右结合把 while 改成递归调用即可一行的事。不要在 Parser 里写死只能处理单个运算符的逻辑课程设计验收时老师很可能会手动输入一个三运算符连用的公式。3.3 语法树的求值接口与变量收集语法树建好之后还需要知道这棵树里出现了哪些命题变元。判别时要把每个变元名映射到这次赋值的真值所以需要一个从语法树收集变量名的过程。我习惯用一次简单的遍历// VarCollector.java class VarCollector { private final SetString vars new LinkedHashSet(); void visit(ExprNode node) { if (node instanceof VarNode) { vars.add(((VarNode) node).name); } else if (node instanceof NotNode) { visit(((NotNode) node).child); } else if (node instanceof BinaryNode) { BinaryNode b (BinaryNode) node; visit(b.left); visit(b.right); } } ListString collect(ExprNode root) { visit(root); return new ArrayList(vars); } }这里用 LinkedHashSet 有两个原因去重的同时保持变量名首次出现的顺序。顺序稳定真值表列序才稳定。否则每次输出列序都随机打印给老师看的时候观感很差。每个语法树节点的求值接口也很简单统一返回布尔值// ExprNode.java abstract class ExprNode { abstract boolean evaluate(MapString, Boolean env); } class VarNode extends ExprNode { final String name; VarNode(String name) { this.name name; } boolean evaluate(MapString, Boolean env) { return env.get(name); // 调用前保证 name 一定在 env 里 } } class AndNode extends ExprNode { final ExprNode left, right; AndNode(ExprNode left, ExprNode right) { this.left left; this.right right; } boolean evaluate(MapString, Boolean env) { return left.evaluate(env) right.evaluate(env); } }注意 VarNode 的 evaluate 里我没有写env.getOrDefault(name, false)。如果你写了兜底默认值变量名拼写错误时程序就会静默取假导致重言式判断错得毫无提示。这里宁可让它抛空指针也要让错误暴露出来。课程设计里最怕的不是报错而是不报错但结果错。4. 真值表判别与输出最小可用的判别主流程解析器把字符串变成语法树变量收集器给出变元列表接下来就是整个重言式判别程序的心脏生成赋值组合、逐行求值、统计结论。这一段写好了程序就能跑通从输入到输出的完整闭环。这个章节是全文里最“抄作业”的段落我会贴出完整可跑的判别主流程。你拿到手把前面章节的节点类合并进来就是一个能运行的 Java 程序。4.1 用位运算遍历 2^n 组赋值拿到 n 个变元后每一行赋值本质上就是一个 n 位二进制数第 i 位为 1 代表第 i 个变元取真第 i 位为 0 代表取假。从 0 遍历到 2^n - 1就覆盖了全部真值组合。用位运算比用计数器加布尔数组快代码也更紧凑// TautologyChecker.java class TautologyChecker { private final ExprNode root; private final ListString variables; TautologyChecker(ExprNode root, ListString variables) { this.root root; this.variables variables; } String checkWithTable() { int n variables.size(); int rows 1 n; // 2^n 行 int trueCount 0; StringBuilder table new StringBuilder(); // 表头 for (String v : variables) { table.append(v).append(\t); } table.append(结果\n); for (int mask 0; mask rows; mask) { MapString, Boolean env new HashMap(); for (int i 0; i n; i) { boolean value ((mask (n - 1 - i)) 1) 1; env.put(variables.get(i), value); } boolean result root.evaluate(env); if (result) { trueCount; } for (int i 0; i n; i) { table.append(env.get(variables.get(i)) ? T\t : F\t); } table.append(result ? T\n : F\n); } if (trueCount rows) { return 该公式是重言式恒真\n table; } else if (trueCount 0) { return 该公式是矛盾式恒假\n table; } else { return 该公式是可满足式非重言式\n table; } } }这段代码里最值得讲的是(mask (n - 1 - i)) 1这个位移方向。如果要让真值表的第一列是第一个变元第二列是第二个变元那么第一个变元应该取 mask 的最高位也就是n - 1 - i作为偏移量。如果你写成mask i那么第一个变元取的是最低位列顺序就反了——真值表会表现为左边第一列跳变最快而不是最右边。这个坑我后面还会细说。4.2 三种判定结论与行数统计重言式的判定标准是全部行为真矛盾式是全部行为假可满足式是至少有一行为真。注意很多同学只实现“重言式”和“其他”两个分支但命题逻辑里还有矛盾式和可满足式的概念。哪怕题目只要求判别重言式把三种结论都区分出来代码的完整度会高一个档次也多不了几行。我把结论判断直接写在checkWithTable里是因为对于一个课程设计规模的真值表这个方案简单直接。如果你想进一步拆层可以把“统计行结果”和“判定结论”拆成两个方法但核心逻辑不变统计trueCount和总行数比较。这里唯一要注意的是总行数rows的计算用了1 n当 n 超过 30 时 int 会溢出课程设计场景基本不会触发。如果你想让程序更稳健可以加一个if (n 25)的提前判断直接拒绝而不是输出溢出后的错误行数。4.3 命令行入口与输出设计主方法负责读入参数、组装各个组件并捕获异常。完整的入口这么写// Main.java public class Main { public static void main(String[] args) { if (args.length ! 1) { System.out.println(用法: java Main \((p-q)(q-r))-(p-r)\); return; } String expr args[0]; try { ExprNode root new Parser(new Lexer(expr)).parse(); ListString vars new VarCollector().collect(root); TautologyChecker checker new TautologyChecker(root, vars); System.out.println(公式: expr); System.out.println(变元: vars); System.out.println(checker.checkWithTable()); } catch (ParseException e) { System.err.println(表达式格式错误: e.getMessage()); } } }运行方式是在命令行传一个带引号的字符串参数例如java Main ((p-q)(q-r))-(p-r)这里有一个 Windows 用户特别容易翻车的点PowerShell 和 CMD 对引号的处理不一样。PowerShell 里传带括号和符号的参数最好用单引号包裹或者用--%终止解析如果直接在 IDE 里跑记得在运行配置里把整个表达式作为一个参数传进去而不是被 IDE 拆成多个参数。遇到“参数数量不对”的报错时先检查这里而不是去改代码。异常处理这里我统一捕获 ParseException但不捕获空指针。如果某个变元没赋值就求值说明变量收集有 bug 或者拼写大小写不一致这种问题应当让程序崩溃并打印堆栈而不是被吞掉后输出一个错误结论。判别类程序最怕“错得莫名奇妙”所以异常策略也应当是“早暴露、早定位”。5. 重言式判别的五个坑从“判不对”到“跑不起来”这一章集中写我在类似课设里反复见到的踩坑点也是你自己动手大概率会撞上的地方。每一条都是真实翻车记录按“现象 → 原因 → 解决”的方式说透。5.1 优先级和括号a||bc差点让我挂了科现象公式p|qr运行后程序判出来的结果和老师手推的不一致而且怎么调都差那么几行。原因不同教材对|和的优先级规定不一致。有的教材规定按“与高于或”处理有的教材规定同级从左到右结合还有的教材对-和-的优先级划分也不同。你的程序按其中一种约定实现但老师默认按另一种约定手推结果自然对不上。解决处理方式分两层。第一层是在代码里严格按照“非 与 或 蕴含 等价”实现这一层文法已经保证了第二层是把你采用的优先级约定明确写进课程设计报告并在程序输出里提示用户表达式建议用括号显式标明结合顺序。你自己测试时也养成全括号输入的习惯比如((p|q)r)彻底规避争议。这本质上不是算法错误而是约定不一致提前声明就能避免验收时的无意义争执。5.2 蕴含联结词实现错位现象公式p-q在 p 为 false 时输出结果错误或者程序直接抛空指针。原因很多同学一看到蕴含就想着写成 Java 的 if 语句if (left) return right; else return true;。这本身没错但有些人会把right.evaluate(env)调用两次或者写完后发现变量作用域对不上。更隐蔽的错误是有人试图把蕴含“翻译”成!left || right时漏了对 left 子树的否定写成left || !right这就变成逆蕴含了。解决在 ImplyNode 的 evaluate 里只用一行return !left.evaluate(env) || right.evaluate(env);。这个变换本身就是逻辑等价式p-q ≡ ¬p∨q是重言式判别的数学依据也是你代码应该遵循的语义。简单、不会错、可读性好不需要走 if 分支。5.3 变量名大小写与重复登记现象输入(P-p)程序真值表输出了 4 行而不是 2 行而且判定结果死活不对。原因Java 的字符串比较是区分大小写的P和p被当成了两个不同变元。更糟的是如果用户在公式里同时用了p和P变量收集器会把它们都收进表里但语义上它们应该代表同一个命题变元。这会让真值表行数无意义地翻倍也会让“重言式”判定看起来像玄学。解决在词法分析那一步统一做处理我在第 3 章 Lexer 里已经写了.toLowerCase()。这比在变量收集器里做归一再保险因为变量名在 Token 阶段就归一化了后续所有环节都不必再考虑大小写问题。如果你使用非英文变量名比如A1和a1这个规则同样适用。5.4 位运算顺序真值表列序全反了现象三个变元p q r输出表格第一列跳变最快最后一列反而是规整的0 0 0 0 1 1 1 1和课本真值表习惯正好相反。原因位运算遍历赋值时写法不对。mask i 1取的是 mask 二进制下的第 i 位最右边是第 0 位所以第一个变元对应的是最低位列序自然反了。真值表习惯上是最左边变元变化最慢对应二进制最高位最右边变元变化最快对应最低位这样表格看起来更符合从高位到低位的二进制递增顺序。解决使用(mask (n - 1 - i)) 1来取位i 为 0 时取最高位对应第一个变元i 为 n - 1 时取最低位对应最后一个变元。这样输出的表格就是最左边变元变化最慢和书本一致老师看着也舒服。5.5 短路求值带来的“假通过”现象程序跑一个明显不是重言式的公式结果却输出“重言式”检查变量收集也看不出问题。原因Java 的和||是短路运算符。如果左操作数已经能决定结果右操作数就不会被求值。对纯布尔逻辑来说短路不影响最终结果但如果你在 evaluate 方法里写了“求值次数计数”“调试日志输出”“字符串拼接”这类副作用短路会让这些副作用统计失真。更危险的场景是某个分支里访问了一个未赋值的变量短路后这段代码没执行异常被静默绕过了于是程序带着残缺数据继续跑。解决在纯判别逻辑里短路本身无害但千万不要在 evaluate 里做副作用操作。如果要统计每个节点的求值次数先对左右子树求值并保存结果再执行统计。例如boolean leftVal left.evaluate(env); boolean rightVal right.evaluate(env); count; return leftVal rightVal;这样即使左值为 false右值也计算过了统计才准确。这条坑属于“写的时候很爽排查的时候很痛”提前注意能省掉一晚上的 debug 时间。6. 让判别程序更可靠随机测试、用例生成与可视化验证跑通一个公式不算完成课设最怕的是“样例全过、验收翻车”。我自己的习惯是写完了判别主流程之后不做手工一条条试而是做一批批量验证用随机生成的公式去轰自己的程序轰不挂才算过关。6.1 随机公式生成器用批量用例验证判别程序手工测试只能覆盖你想得到的情况而随机生成器能覆盖你想不到的嵌套、多层否定和混合优先级。写一个递归生成合法公式的代码String randomFormula(Random rnd, int depth) { if (depth 0) { String[] vars {p, q, r, s}; return vars[rnd.nextInt(vars.length)]; } int op rnd.nextInt(5); switch (op) { case 0: return ! randomFormula(rnd, depth - 1); case 1: return ( randomFormula(rnd, depth - 1) randomFormula(rnd, depth - 1) ); case 2: return ( randomFormula(rnd, depth - 1) | randomFormula(rnd, depth - 1) ); case 3: return ( randomFormula(rnd, depth - 1) - randomFormula(rnd, depth - 1) ); default: return ( randomFormula(rnd, depth - 1) - randomFormula(rnd, depth - 1) ); } }然后用这个生成器跑一百个公式每个公式都要求程序不抛异常、真值表行数等于 2 的变元个数次方、三种结论必居其一。这能同时检验解析器和判别器的健壮性。6.2 组合恒等式自查法随机测试之后别忘了验证结果的正确性。我建议手工准备三组公式一组确定是重言式一组确定是矛盾式一组确定是普通可满足式。确定是重言式的例子包括p|!p、(pq)-p、((p-q)(q-r))-(p-r)确定是矛盾式的例子包括p!p、(p-q)(p!q)可满足式可以用p|q、p-q这类。程序对这几种公式的输出结果必须精确匹配预期任何一行都不许差。这里还有一个特别值得加进去的测试把教材上的等值公式(p-q)-(!p|q)作为输入应该输出重言式因为p-q本来就和!p|q逻辑等值。如果这个公式判错了说明蕴含节点的实现有反向问题这是最灵敏的一条回归用例。6.3 给课程设计报告的结果导出最后一步是把结果输出成文件方便写课程设计报告时直接引用。常见做法是支持重定向到文本文件写成 CSV 也可以方便放进 Excel// 在 TautologyChecker 中追加一个导出方法 void exportToFile(String results, String path) throws IOException { try (PrintWriter writer new PrintWriter(new FileWriter(path))) { writer.write(results); } }调用后在同目录生成 truth_table.txt真值表以制表符分隔粘贴进 Word 或 Excel 后对齐效果也不错。如果你的报告需要展示程序处理不同公式的对比把几个公式的结果都导出成单独文件页面里放拼接截图就够了。到现在这条路其实很清楚了重言式判别程序的技术难点不在判别本身而在输入解析的严谨和输出结果的可靠上。把表达式解析的优先级和括号吃透了剩下的就是一套遍历统计的事。我自己当年第一次跑通这个课设时就折在列序反了还硬着头皮往下加功能整个下午的输出表格全是反的最后才发现只是 i和 (n-1-i)的差别。从那以后我写判别类程序第一件事永远是先把已知的重言式、矛盾式各列三组做回归跑通了再碰新功能。希望这个习惯和这整套代码思路能帮你在提交重言式判别程序时少走几段弯路。本文还有配套的精品资源点击获取