NCHUD-数字电路模拟程序三次作业总结

【标题】NCHUD-数字电路模拟程序三次作业总结

这三次作业是围绕数字电路模拟这个场景一步步加深的,从最开始只有五种基础逻辑门,到后来加入三态门、译码器、数据选择器等复杂组合元件,最后还要支持子电路定义和异常输入检测。整个过程让我把刚学的面向对象知识和数据结构算法用了个遍,特别是递归求值、记忆化搜索和组合模式。

第一次作业是最基础的,就是做一个简单的数字电路模拟。要定义与门、或门、非门、异或门、同或门五种元件,每个元件有自己的输入引脚和输出引脚,然后从键盘输入外部信号和连接关系,程序自动计算出每个元件的输出电平。那时候刚接触递归求值,觉得还挺新鲜的,不过也踩了不少坑,比如元件命名格式`A(8)1`和`X5`两种写法混在一起解析的时候特别容易出错。引脚连接关系是用`[A A(8)1-1 A(8)1-3 X5-2]`这样的格式描述的,要正确解析出输出引脚和多个输入引脚。求值用的是递归加记忆化缓存,避免重复计算。输出要求按与门、或门、非门、异或门、同或门的顺序排列,同类元件按编号升序。总的来说第一次作业代码大概250行,三个核心数据结构加若干工具方法,难度适中,算是热身,熟悉了Java的字符串解析、Map/List集合和递归算法的基本用法。

第二次作业就复杂多了。题目要求新增三态门、译码器、数据选择器、数据分配器四种元件,元件类型从5种变成了9种。而且这些新元件的引脚类型不再简单——三态门有控制引脚、输入引脚、输出引脚;译码器有控制引脚、输入引脚、多个输出引脚;数据选择器和数据分配器也各有各的引脚分配规则。最头疼的是输出格式也五花八门:基础门输出`A(2)1-0:1`这种格式,译码器要输出激活的引脚编号如`M(3)1:3`,数据分配器要按引脚顺序输出一串电平如`F(3)1:-1------`。这意味着不能像第一次那样统一处理输出了,每种元件都得单独写输出逻辑。引脚编号规则也需要仔细处理,比如译码器`M(3)1`的0/1/2号是控制引脚,3/4/5号是输入引脚,6~13号是输出引脚。这次作业写了大约400行,枚举类型从5个扩展到9个,新增了`isOutputPin()`和`computeGateOutput()`两个核心方法。最头疼的是要理清楚每种元件的引脚编号范围和输出格式,尤其是译码器的输出引脚编号计算涉及到`1 << inputCount`的位移运算,稍微不注意就算错。不过做完后确实对枚举类型的设计、复杂字符串解析的理解深了不少。

第三次作业是终极版,加入了子电路和异常输入检测。子电路可以像元件一样在主电路中被引用,这涉及到组合模式的设计思想。子电路的输入输出格式是`C1: INPUT: A B OUT: C`这样定义的,主电路中可以用`C1-A`来引用子电路的引脚。异常检测部分更复杂,要检测五种异常情况:连接包含多个输入、连接无输入、连接无输出、输入输出顺序错误、输入引脚信号冲突,而且异常之间有严格的优先级顺序。这次作业的代码结构做了比较大的调整,引入了`ExceptionTracker`、`CircuitConfig`、`SimulationEngine`、`CircuitParser`等多个专职类,代码量大约550行,十个类,三十多个方法。最难的部分是异常优先级的处理——如果一条输入出现了多种异常,要按优先级输出最高的那个,这就要求在检测异常时按优先级从低到高注册,输出时再按相反顺序弹出。还有就是子电路的求值逻辑,需要把子电路看作一个整体,内部元件独立求值后再把结果传递到上层。通过这三次作业,自己的编程能力进步明显。第一次还在懵懵懂懂地写字符串解析,第二次开始知道怎么用枚举管理类型信息了,第三次能独立完成一个有一定规模的小系统。而且深刻体会到了代码可扩展性的重要性——第一次作业把解析逻辑全写在`parseGate()`的if-else里,到了第二次就膨胀得很难维护了,如果从一开始就用工厂模式或者策略模式,后面扩展会轻松很多。另外,也学会了怎么用递归加记忆化处理DAG求值、怎么设计多级优先级的异常处理、怎么处理复杂的输出格式。题量从250行增加到550多行,类从3个核心数据结构增加到10个专职类,难度也是一步步提升,但因为有前面两次的基础,第三次虽然复杂但并没有让我完全无从下手。总之,这次系列作业把课堂上学到的数据结构和算法真正用到了实践中,也让我对代码架构设计有了更深的理解。

【标题】Complexity Metrics(复杂度分析)

因为下面要用到复杂度分析,所以先在此给出一些相关概念。

我们需要使用的主要是方法和类的复杂度分析。方法的复杂度分析主要基于循环复杂度的计算。循环复杂度是一种表示程序复杂度的软件度量,由程序流程图中的"基础路径"数量得来。

- ev(G):即Essential Complexity,用来表示一个方法的结构化程度,范围在[1,v(G)]之间,值越大则程序的结构越"病态",其计算过程和图的"缩点"有关。

- iv(G):即Design Complexity,用来表示一个方法和他所调用的其他方法的紧密程度,范围也在[1,v(G)]之间,值越大联系越紧密。

- v(G):即循环复杂度,可以理解为穷尽程序流程每一条路径所需要的试验次数。

对于类,有OCavg和WMC两个项目,分别代表类的方法的平均循环复杂度和总循环复杂度。

下面我将从程序结构、公测与Bug分析几个方面来总结我这一系列的三次作业。

第一次作业

作业要求

设计一个基础的数字电路模拟模块。系统需要支持与门、或门、非门、异或门、同或门五种逻辑门元件。用户可以定义外部输入信号和元件之间的连接关系,系统自动计算每个元件的输出电平。类设计必须符合单一职责原则(SRP)。

实现方式

1. 使用BufferedReader逐行读取输入,以end为结束标志。

2. 解析INPUT:行,将外部信号存入extInputs(Map<String, Integer>)。

3. 解析[ ... ]连接行,提取输出引脚和输入引脚列表,存入connections。

4. 遍历所有连接,识别其中的元件名,调用parseGate()解析并存入gates(Map<String, Gate>)。

5. 遍历所有连接,构建driverOf映射(输入引脚 → 驱动源引脚)。

6. 实现evaluate(String pin)递归求值方法:

- 不含"-"的引脚视为外部输入,直接从extInputs取值。

- 输入引脚(引脚号>0)通过driverOf找到驱动源,递归求值。

- 输出引脚(引脚号==0)收集所有输入引脚的值,调用gate.compute()计算。

- 使用memo缓存已求值结果,避免重复计算。

7. 按元件类型顺序(AND、OR、NOT、XOR、XNOR)和编号升序排序输出。

代码规模

第一次作业代码规模如下图

类图

第一次作业的类图如下:

复杂度分析

第一次作业的复杂度分析如下:

高复杂度方法的原因:parseGate()的复杂度来源于元件命名规则的多样性,无法避免,但可以将其封装在单独的方法中,这正是当前设计所做的。evaluate()的递归逻辑是业务流程需要,且复杂度在可控范围内。

Bug分析

公测

程序在公测中没有出现正确性错误,所有测试点均通过。但代码存在以下可改进之处:

- 所有逻辑集中在Main类中,虽然使用了静态内部类,但整体耦合度较高。

- 代码中注释较少,对于后期维护可能造成困难。

- parseGate()的if-else链较长,可读性一般。

互测

在互测阶段,没有被发现功能性bug。但通过阅读其他同学的代码,发现了几个常见问题:

- 引脚解析错误:部分同学使用indexOf('-')而非lastIndexOf('-')分割引脚,导致元件名中包含连字符时解析错误(如A(8)1-0会被错误分割)。

- 递归死循环:少数同学在evaluate()中没有正确处理循环依赖(虽然题目保证无环),但缺乏防护措施。

- 记忆化缓存失效:有些同学在递归求值时没有使用memo缓存,导致大量重复计算,在大规模电路下性能下降明显。

- 输出排序错误:部分同学没有按元件类型和编号正确排序,导致输出顺序与要求不符。

在互测中尝试构造边界测试用例(如单输入与门、多输入或门、非门级联等),未发现逻辑错误。说明本人代码的正确性较好。

测试方法

本次作业采用的测试策略如下:

- 手动构造边界数据:例如单输入与门、8输入或门、非门级联、异或门和同或门的对比测试等。

- 逐步调试法:对于每个测试用例,手动模拟信号传播过程,与程序输出逐行对比。

- 代码审查:在互测阶段,认真阅读他人代码,重点检查引脚解析的正确性、递归求值的终止条件、输出排序的逻辑等。

- 格式化输出验证:由于输出格式要求严格(元件名-0:值),使用字符串拼接确保格式正确。

第二次作业

作业要求

在第一次作业的基础上,新增三态门、译码器、数据选择器、数据分配器四种组合电路元件。元件类型从5种扩展到9种。新增元件的引脚类型多样化(控制引脚、输入引脚、输出引脚),输出格式也各不相同。系统需正确处理元件的有效/无效状态(如三态门的高阻态、译码器的无效控制状态)。

实现方式

1. 扩展GateType枚举,新增TRI_STATE、DECODER、MUX、DEMUX。

2. 扩展parseGate()方法,支持解析S(三态门)、M(译码器)、Z(数据选择器)、F(数据分配器)。

3. 新增isOutputPin(Gate gate, int pinNum)方法,判断一个引脚是否为输出引脚:

- 基础门:引脚0为输出

- 三态门:引脚2为输出

- 译码器:引脚从inputCount+3到inputCount+3+(1<<inputCount)-1为输出

- 数据选择器:引脚inputCount + (1 << inputCount)为输出

- 数据分配器:引脚从inputCount+1到inputCount+1+(1<<inputCount)-1为输出

4. 新增computeGateOutput(Gate gate, int pinNum)方法,专门处理各类型元件的输出计算:

- 三态门:控制引脚为0时返回null(高阻态),否则返回输入值

- 译码器:检查控制引脚S1=1且S2+S3=0,计算输入编码k,返回激活引脚的值(0)和其他引脚的值(1)

- 数据选择器:计算控制编码k,选择对应的数据输入引脚

- 数据分配器:计算控制编码k,将输入信号送到对应的输出引脚,其他输出引脚为null

5. 差异化输出逻辑:

- 基础门和三态门:输出元件名-引脚号:值

- 译码器:输出元件名:激活引脚编号

- 数据分配器:按引脚顺序输出电平字符串,无效状态显示为"-"

代码规模

第二次作业代码规模如下图

类图

第二次作业的类图如下:

复杂度分析

第二次作业的复杂度分析如下:

Bug分析

公测

程序在公测中通过了所有测试点。但在实现过程中遇到了以下问题:

- 译码器输出引脚编号计算错误:初始实现中,我将译码器的输出引脚起始地址计算为inputCount,忽略了3个控制引脚的存在,导致输出引脚编号偏移。通过调试才确认正确的计算公式为inputCount + 3。

- 数据选择器输出引脚号错误:数据选择器的输出引脚号为inputCount + (1 << inputCount),初始实现中误写为inputCount + inputCount。

- 三态门高阻态处理:最初使用-1表示无效状态,但与有效信号0冲突,后改为返回null。

互测

在互测阶段,没有被发现功能性bug。但通过阅读其他同学的代码,发现了几个常见问题:

- 引脚编号规则理解错误:部分同学对译码器、数据选择器的引脚编号规则理解有误,导致引脚映射错误。

- 输出格式错误:译码器应输出M(3)1:3格式,部分同学错误地输出了M(3)1-0:1这样的格式。

- 数据分配器字符串顺序错误:数据分配器要求按引脚编号从小到大输出,部分同学颠倒了顺序。

- 控制引脚有效条件错误:译码器的有效条件是S1=1且S2+S3=0,部分同学误写为S1=1且S2=0且S3=0。

在互测中重点构造了译码器控制无效、三态门控制为0、数据选择器全0控制等边界用例,未发现逻辑错误。

测试方法

本次作业采用的测试策略如下:

- 逐元件测试:针对每种新增元件单独构造测试用例,验证其功能正确性。

- 组合电路测试:将多种元件组合成复杂电路,验证信号传递的正确性。

- 边界条件测试:测试译码器控制无效、三态门高阻态、数据分配器全0控制等边界情况。

- 输出格式验证:使用字符串精确匹配验证输出格式是否符合要求。

第三次作业

作业要求

在第一次作业的基础上(注意:本次作业跳过了程序-3,直接在程序-1基础上迭代),新增子电路定义与引用、异常输入检测两大功能。子电路可以像元件一样在主电路中被引用,支持嵌套。异常检测包括5种类型,且有严格的优先级顺序。

实现方式

1. 子电路解析:

- 识别C编号:标记,开始解析子电路

- 解析子电路内部的INPUT:和OUT:定义

- 解析子电路内部的连接信息

- 以endc为结束标志

- 子电路信息在主电路之前全部解析完成

2. 子电路引用:

- 主电路中通过C2-A语法引用子电路的引脚

- 子电路元件输出时带上子电路编号:C1-A(2)1-0:0

3. 异常检测(按优先级从高到低):

- 连接信息包含多个输入:ERROR: [连接信息] include more than one input

- 连接信息无输入:ERROR: [连接信息] include none input

- 连接信息无输出:ERROR: [连接信息] include none output

- 输入输出顺序错误:ERROR: [连接信息] input and output sequence error

- 输入引脚信号冲突:ERROR: 输入引脚 input signal conflict

4. 代码重构:

- 引入ExceptionTracker类管理异常

- 引入CircuitConfig类管理电路配置

- 引入CircuitParser类负责解析

- 引入SimulationEngine类负责仿真

- 引入OutputFormatter类负责输出格式化

代码规模

第三次作业的代码规模如下图

类图

第三次作业的类图如下:

复杂度分析

第三次作业的复杂度分析如下:

Bug分析

公测

程序在公测中通过了所有测试点。但在实现过程中遇到了以下问题:

- 子电路解析顺序:子电路信息必须在主电路之前定义,初始实现中没有正确处理这个顺序,导致子电路未被正确识别。

- 子电路输出命名:子电路元件的输出需要带上子电路编号(如C1-A(2)1-0),初始实现中遗漏了编号前缀。

- 异常优先级处理:多条异常同时存在时,需要按优先级输出最高的一条。初始实现中使用栈存储异常,但注册顺序与优先级顺序不一致,导致输出错误。

互测

在互测阶段,没有被发现功能性bug。但通过阅读其他同学的代码,发现了几个常见问题:

- 子电路嵌套处理:部分同学没有正确处理子电路的嵌套引用。

- 异常检测遗漏:部分同学漏掉了某些异常类型的检测。

- 引脚冲突检测:输入引脚冲突需要在所有连接解析完成后统一检测,部分同学在解析过程中即时检测,导致漏报。

- 错误信息格式:异常信息的格式必须完全匹配题目要求,部分同学在细节上有出入。

在互测中重点构造了多种异常组合的测试用例,验证异常优先级处理是否正确。

测试方法

本次作业采用的测试策略如下:

- 子电路功能测试:构造包含单个子电路、多个子电路、嵌套子电路的测试用例。

- 异常逐项测试:针对5种异常类型分别构造测试用例。

- 异常优先级测试:构造同时包含多种异常的连接信息,验证优先级处理。

- 边界条件测试:测试空子电路、无输出子电路、子电路编号冲突等边界情况。

总结

通过这三次作业,自己的编程能力进步明显。第一次还在懵懵懂懂地写字符串解析,第二次开始知道怎么用枚举管理类型信息了,第三次能独立完成一个包含子电路和异常处理的小系统。

深刻体会到的几点:

一、代码可扩展性的重要性

第一次作业把解析逻辑全写在parseGate()的if-else里,到了第二次就膨胀得很难维护了。如果从一开始就用工厂模式或者策略模式,后面扩展会轻松很多。第三次作业尝试引入了多个专职类,虽然仍有不足,但已经比前两次好很多。

二、递归+记忆化是处理DAG求值的利器

数字电路本质上是一个有向无环图(DAG),evaluate()方法的递归-记忆化实现是典型的DAG求值模式。这种模式在图计算、表达式求值等领域有广泛应用,值得深入掌握。

三、异常处理需要系统性地设计

第三次作业的异常处理让我认识到:一个健壮的系统不仅需要正确处理正常流程,还需要系统地管理异常情况。异常优先级的处理更是考验了对需求的理解和数据结构的设计能力。

四、组合模式的价值

虽然第三次作业中我未能完整实现组合模式,但通过题目提示,我理解了组合模式在处理"部分-整体"层次结构时的优势——将子电路和基本元件统一对待,简化客户端代码。

需要进一步学习的内容:

- 设计模式的系统学习:工厂模式、策略模式、组合模式、访问者模式等,提升代码的架构设计能力。

- 递归下降解析器:作业中的输入格式越来越复杂,递归下降解析器是处理嵌套语法的标准工具。

- 单元测试:三次作业主要依靠手动测试,效率低下且覆盖不全,需要学习JUnit等测试框架。

- 并发编程:后续涉及时序电路和反馈电路,可能引入时钟信号和状态保持的概念。

题量从250行增加到550多行,类从3个核心数据结构增加到7个专职类,难度也是一步步提升。但因为有前面两次的基础,第三次虽然复杂但并没有让我完全无从下手。总之,这次系列作业把课堂上学到的数据结构和算法真正用到了实践中,也让我对代码架构设计有了更深的理解。

posted @ 2026-06-22 21:00  龙炳宇  阅读(9)  评论(0)    收藏  举报