PTA 4-6总结

数字电路模拟程序系列作业总结
从三次迭代作业中,打造了一个逐渐复杂的数字电路模拟器。从最初只能处理几个基本逻辑门,到支持多输入输出组合元件,最后引入子电路和异常检测,每一步让我重新审视设计的扩展性与鲁棒性。这篇文章将按三次作业的顺序,详细梳理我的设计思路、实现细节、复杂度分析以及踩过的坑,最后谈谈我对设计模式的思考。

从这三个作业我学到了什么
面向对象的抽象能力
元件不再是一堆离散的函数,而是一个层次化的类体系。基类定义接口,派生类实现具体逻辑,让我能轻松扩展新的门类型而无需修改核心调度代码。

设计模式是“长出来”的
第三次作业要求支持子电路,真正体会到“组合模式”的优雅。把子电路和基本门看作同一类事物,让主电路无需关心内部细节,极大简化了连接和求值流程。

正则表达式与文本解析
引脚描述如 A(8)1-2、M(3)1-0,必须用健壮的正则抓取标识符、参数和编号。稍不注意就会留下解析漏洞。

图算法与拓扑排序
电路本质是有向无环图(不考虑反馈时),求值顺序必须遵守依赖关系。用拓扑排序或递归记忆化搜索能避免重复计算,同时检测环路。

异常处理的优先级与短路
第三次作业规定了多种异常及其优先级,要求遇到第一条异常立刻停止处理。这让我学会在解析阶段分层检查,保证错误信息精确且符合规范。

迭代中的重构
每次作业都在前一次的基础上增加大量需求,如果不及时重构,代码会迅速腐烂。适时的类拆分、工厂引入,让第二次作业的元件爆炸和第三次的组合模式都有落脚点。

整体复杂度概览
三次作业的核心算法都是 基于拓扑排序的信号传播,时间复杂度为 O(N+E),其中 N 为引脚/元件节点数,E 为连接数。因为每个元件的计算逻辑是纯组合的(无反馈),只需一次拓扑遍历即可得到所有稳定输出。

圈复杂度主要集中在输入解析和元件计算函数。解析函数需要处理多种格式,分支较多;而各元件的计算函数逻辑简单,圈复杂度较低。

空间复杂度 O(N),存储元件图、连接表和信号值。

下面分三次作业详述。

数字电路模拟程序 1 —— 基础逻辑门
作业要求
支持与门 A(n)、或门 O(n)、非门 N、异或门 X、同或门 Y。

输入格式:INPUT: A-1 B-0 定义外部输入信号。

连接格式:[输出引脚 输入引脚列表],如 [A A(2)1-1 A(2)1-3]。

输出:按 A O N X Y 顺序,同类元件按编号升序,输出 元件名-0:值。未完全连接的元件忽略。

实现方式
解析器
利用正则 ([AONXY])((\d+))?(\d+) 提取元件类型、输入引脚数和编号。
连接行用 [(.*?)] 捕获,再按空格分割得到引脚列表,第一个为源输出,后续为目的输入。

数据结构

Gate 抽象类:name, inputPins (Map<Integer, Signal>), output。定义 abstract int compute()。

AndGate, OrGate, NotGate, XorGate, XnorGate 实现 compute()。

Circuit 类:持有 Map<String, Gate> gates,Map<String, List> connections。

求值流程
构建有向边:源输出 → 目标输入引脚。使用 拓扑排序(Kahn算法)确定计算顺序,或直接递归求值(用 HashMap 记忆化,遇环则报错)。外部输入作为初始信号源加入队列。计算完成后遍历所有门,若某门的全部输入均已赋值,则计算输出并传递给下游。

输出排序
按类型优先级映射(A=0,O=1,N=2,X=3,Y=4),同类型按编号数字排序,忽略输入不全的门。

代码规模
9d5bee35fa3e6fffcacb743da76431f2

类图
e7dad8a60192faf2b1b54aa159013c87

复杂度分析

ff21f83642a6131a0865945c061c91cc

解析器:圈复杂度约 8,主要来自多重 if-else 判断元件类型。

拓扑求值:O(N+E)。

空间:元件和连线表,约 O(N)。

Bug 分析与公测/互测
遇到的Bug:

引脚编号未处理连续性与从1开始,导致数组越界。

未考虑同名输入引脚可能被多个输出驱动(作业1未规定异常,按覆盖处理,但埋下了隐患)。

输出排序时将编号当字符串排序,导致“10”排在“2”前面,需要用自然排序。

公测/互测:
组内设计边界用例,如多输入与门全1、全0,异或门不同组合。互测时重点攻击错误格式和缺失连接的角落。

测试方法
采用JUnit单元测试,为每种门写独立测试,然后用文件输入模拟完整电路做集成测试。构造特殊场景:极高扇出的输出、未连接引脚的元件,观察是否正确忽略。

数字电路模拟程序 2 —— 多输入输出组合电路元件
作业要求
新增元件:三态门 S、译码器 M(n)、数据选择器 Z(n)、数据分配器 F(n)。

引脚类型区分:控制引脚、输入引脚、输出引脚,编号顺序为 控制→输入→输出。

三态门:控制=1导通输出=输入,控制=0高阻(无效)。

译码器:控制有效时(S1=1, S2+S3=0),根据输入编码唯一输出0,其余输出1;否则全部无效。

数据选择器:控制端选择一路输入送至输出。

数据分配器:控制端选择一路输出等于输入,其余输出无效(-)。

输出格式:译码器输出 标识符(输入引脚数)编号:输出0的引脚号;数据分配器按顺序输出所有输出引脚电平,无效为 -;其他门同作业1;无效元件忽略。

实现方式
元件体系重构
引入 PinType 枚举(CONTROL, INPUT, OUTPUT)。Gate 增加 List controlPins, List inputPins, List outputPins。利用工厂模式根据标识符创建不同 Gate 子类。

引脚编号映射
以译码器 M(3)1 为例:控制引脚02,输入35,输出6~13。解析时根据元件类型和参数生成内部引脚映射表。

连接与求值
基本逻辑不变,但计算时需先判断控制引脚是否有效。例如三态门控制引脚未接或为0,则输出记为 INVALID;译码器控制不满足条件则所有输出无效。对于数据分配器,输出是一个固定长度的数组,无效位置填充特殊值,输出时转为 -。

输出格式化
利用多态,基类定义 String formatOutput(),各子类覆写。译码器返回 M(2)1:0;分配器返回 F(2)1:--0-;其余门返回 A(4)1-0:1 等。

代码规模
ce922183c4e159a9ed4fabad527d3aa8

类图

83bc23086e8be3366f8a6f859645c9cf

复杂度分析
e5236a3633e65fa2ce9cf2db22ef9d18

译码器、分配器的输出数量随输入位数指数增长,但实际计算仍为 O(1) 每元件。

求值调度不变,整体 O(N+E)。

解析器圈复杂度上升至约 12,因为增加了对新元件类型的判断和引脚顺序处理。

Bug 分析与互测
引脚索引偏移:控制引脚从0开始,输入紧接其后,拼接时容易差一错误。通过单元测试逐元件验证纠正。

译码器有效条件:题目要求 S1=1, S2+S3=0,即S2和S3必须同时为0。第一次误写为只要和为0即有效,导致 S2=1, S3=1 也有效(和=2≠0,倒是没问题,但 1+ -1? 信号只有0/1)。更隐患的是 S2 未连接时可能引发空指针,需要提前判无效。

分配器输出顺序:输出引脚编号从小到大对应输出字符串从左到右,需保证索引映射正确。

公测中发现其它同学的Bug主要集中在对“无效元件忽略”理解不一致,有的输出了 x 或留空,导致格式不匹配。

测试方法
为每一种新元件编写参数化测试,覆盖所有控制组合、边界编码。使用脚本批量生成随机电路,对比手算结果进行回归测试。

数字电路模拟程序 3 —— 子电路与异常检测
(本题基于基础逻辑门,但增加了子电路和异常输入处理)

作业要求
子电路:以 C编号: 开始,包含 INPUT:、OUT:、连接信息,以 endc 结束。子电路内部元件仍为五种基本门。

主电路可通过 子电路编号-引脚名 引用子电路的输入/输出(如 C1-A)。

输出时,子电路内部元件的引脚带前缀 C1-,如 C1-A(2)1-0:0。

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

连接信息包含多个输出源 → ERROR: [...] include more than one input
连接信息无输入引脚 → ERROR: [...] include none input
连接信息无输出引脚 → ERROR: [...] include none output
连接信息输入输出写反 → ERROR: [...] input and output sequence error
一个输入引脚被多个输出驱动 → ERROR: pin input signal conflict
优先级:同一条连接信息出现多种异常时,报优先级最高的。多条连接有异常,只报第一条。

实现方式
组合模式
定义抽象 Component,包含 evaluate() 和 getOutput(pinName) 方法。

LeafGate:封装基本逻辑门。

CompositeCircuit:子电路,内部持有 List 和内部连线表,以及外部可见的输入输出引脚映射。
主电路本身也是一个 CompositeCircuit。

名称空间与引脚映射
每个 CompositeCircuit 有一个 prefix(如 C1-)。子电路内部的连接在解析时自动加上前缀,与外部主电路隔离。当主电路引用 C1-A 时,会通过子电路的输入引脚映射找到内部实际输入节点,并将外部信号传递进去;引用 C1-C 输出时,则把内部输出引脚的值暴露给外部。

异常检测流程
在解析每一条连接信息 [ ... ] 时:

扫描所有引脚,区分哪些是“电路外部输入/元件输出”(属于源),哪些是“元件输入引脚”(属于目标)。判断依据:引脚名是否为纯字母(如 A)或以 -0 结尾(元件的输出引脚0),或通过注册的元件引脚类型表查询。

若源引脚数量 > 1 → 异常1。

若目标引脚数量 == 0 → 异常2(因为没有任何输入)。

若源引脚数量 == 0 → 异常3(没有任何输出)。

若第一个引脚是目标类型(即输入输出写反)→ 异常4。

处理完所有连接后,检查每个输入引脚是否被多个源驱动 → 异常5。

一旦捕获到任何异常,立即打印并终止程序(忽略后续输入)。

子电路求值
递归计算:主电路求值前,先递归求出所有引用的子电路的输出。子电路内部依然采用拓扑排序,确保无环。最终输出收集所有 Component(包括子电路内的 LeafGate),按类型和编号全局排序。

代码规模
约 1500 行。组合模式的引入增加了抽象层次;异常检测占据约 200 行。

类图
e336a19c3d2a287472f90df48c0afd78

Gate 复用作业1的层次,LeafGate 作为适配器。主电路和子电路都是 CompositeCircuit 的实例,实现递归组合。

复杂度分析
986d033a27a00fe66c79677a60889185

异常检测与解析耦合,圈复杂度较高(约 15),但通过提取方法可控制。

子电路嵌套会带来递归求值开销,但深度有限,总复杂度仍 O(N+E)。

空间复杂度随子电路数量线性增长。

Bug 分析与公测/互测
我遇到的Bug:

子电路内部元件与主电路同名编号冲突:忘记加前缀导致覆盖,通过组合模式的前缀机制解决。

异常4的判断:误将“源引脚数量为1但目标数量也为1且源在后”判为异常4,实际上只要第一个是目标类型即可判定顺序错误。

子电路输出引脚在主电路未连接时不应参与全局输出,但初始实现将其作为有效输出打印,修正为未连接外部驱动的输出视为无效忽略。

公测/互测:
这次异常优先级是重灾区。许多同学在同一条连接既无输入又无输出时,报了低优先级的异常。需严格按顺序逐项 if-else if 检查。互测时我构造了大量嵌套子电路和异常边界用例,如空子电路、子电路输出接子电路输入,确保稳定性。

测试方法
编写异常专项测试:手动构造每一种异常及组合,验证错误信息和优先级。

对子电路采用集成测试:在主电路中以不同连接方式使用子电路,并与展开等效平面电路对比结果。

随机测试:脚本随机生成合法电路及其子电路变体,自动比对计算结果。

关于设计模式的思考
完成三次作业后回看,最深的体会是 设计模式让迭代变得自然。

组合模式(Composite)
第三次作业引入子电路时,组合模式几乎是唯一正解。把子电路和基本门同等对待,使得连接、求值、输出都可以用统一接口处理。主程序不需要 if (isSubCircuit) ... else ... 的分支,极大降低了心智负担。这也为后续扩展(如多层嵌套子电路)铺平了道路。

工厂模式(Factory)
在第二次作业元件种类爆炸时,用工厂方法根据标识符和参数动态创建 Gate 对象,把创建逻辑与业务逻辑解耦。新增元件只需在工厂里注册,不影响其它模块。

策略模式(Strategy)
我并没有显式使用,但回头看,元件的 compute() 本质上就是一系列策略。如果未来需要支持不同工艺(如正逻辑/负逻辑),可以抽象出信号计算策略。

适配器模式(Adapter)
LeafGate 其实就是对 Gate 类的适配,让它符合 Component 接口。这让我能复用作业1的 Gate 类而不用大幅修改。

单一职责与开闭原则
解析器只负责文本到模型的转换,电路只负责信号传播,元件只负责逻辑计算。新增元件对解析器是封闭的(只需改工厂),对电路调度则是完全透明的。

这三次作业像一次微缩版的软件演进史。从“能跑就行”到追求结构优雅,再到应对需求变更,每一次重构都让我更理解设计模式背后的设计原则。希望这份总结能为后续的迭代(如时序电路、反馈环路)打下扎实的基础。

posted @ 2026-06-24 21:19  ihua  阅读(3)  评论(0)    收藏  举报