第一次作业:先让最简单的电路跑起来
我踩过的最大的坑
这次作业我自己测试时踩了两个巨坑,差点交不上。第一个是元件名字解析错误,一开始我用split方法拆 "A (8) 1" 这种名字,先按 "(" 拆再按 ")" 拆,结果遇到 "N1" 这种没有括号的名字,直接数组越界崩溃。第二个是信号传播顺序搞反了,我先算所有门的输出再传信号,结果用的是上一轮的旧值,最简单的与门输入 1 和 1 居然输出 0,改了半天才反应过来。
这次要做什么
实现五种最基础的逻辑门:与门、或门、非门、异或门、同或门,能读取电路的输入和连接信息,计算出所有门的输出值
我当时怎么想的
完全没考虑什么设计模式,就是怎么简单怎么来。用了一个通用的LogicGate类把所有五种门都装进去,相当于一个万能盒子,只是给不同的参数区分类型,主类统一管理所有元件和连接,流程控制全写在 main 方法里。
核心逻辑
用了最笨也最简单的迭代式信号传播算法:一遍又一遍地扫整个电路,先把所有能传的信号从输出引脚传到输入引脚,再把所有输入接全了的门算出来输出。直到扫了一遍之后没有任何信号变化,说明电路稳定了,就可以输出结果了
代码结构
static class LogicGate{
char type;
int id;
int inputCount;
int output = -1;
HashMap<Integer, Integer> pinValues = new HashMap<>();
String fullName;
LogicGate(char t, int i, int c){
type = t;
id = i;
inputCount = c;
}
}
public static void main(String[] args){
// 解析输入
parseInput(line);
// 创建所有门电路
createGates(connection);
// 迭代计算直到稳定
do{
updated = false;
updatePinValues();
calculateAllOutputs();
}while(updated);
// 输出结果
printResults();
}
static void calculateOutput(LogicGate gate){
if(gate.type == 'A'){
int result = 1;
for(int v : gate.pinValues.values()){
if(v == 0){ result = 0; break; }
}
gate.output = result;
}else if(gate.type == 'O'){
int result = 0;
for(int v : gate.pinValues.values()){
if(v == 1){ result = 1; break; }
}
gate.output = result;
}else if(gate.type == 'N'){
gate.output = gate.pinValues.get(1) == 0 ? 1 : 0;
}else if(gate.type == 'X'){
gate.output = gate.pinValues.get(1) != gate.pinValues.get(2) ? 1 : 0;
}else if(gate.type == 'Y'){
gate.output = gate.pinValues.get(1) == gate.pinValues.get(2) ? 1 : 0;
}
}
复杂度分析


第一次作业我写得特别糙,把所有门的计算都塞到一个calculateOutput方法里了。虽然各方法复杂度都不高,但扩展性极差,后来加新元件的时候差点没累死
类图

时序图

bug 分析
强测和互测都没发现 bug,自己测试时的两个坑都改过来了。所有 6 个测试样例一次性全过,包括那个输入不全自动忽略的样例
第二次作业:一堆复杂元件
我踩过的最大的坑
译码器的引脚编号问题!这个坑我这辈子都忘不了,题目里写了一句 "按控制 - 输入 - 输出的顺序排序",我当时扫了一眼就过去了,以为输入引脚从 0 开始。结果所有译码器的测试用例全错,输出的数字永远比样例大 3。我从晚上把译码器的代码翻来覆去看了几十遍,甚至怀疑题目给的样例是错的。最后又回去读了一遍题目,一个字一个字地抠,才发现原来控制引脚占了 0、1、2 三个位置,输入引脚是从 3 开始的。我把所有的引脚编号都加了 3,再一运行,居然全对了
这次要做什么
在第一次作业的基础上,新增四种复杂元件:三态门、译码器、数据选择器、数据分配器。还要处理控制引脚、多输出引脚,以及三种完全不同的输出格式
我当时怎么想的
第一次作业那个万能的LogicGate类已经完全不能用了,每个新元件的计算逻辑都不一样,引脚数量和编号规则也不一样。我已经来不及重构了,只能硬着头皮拆成了九个独立的类,每个元件一个类。还是没有用什么设计模式,就是面向完成编程,能跑通就行
核心逻辑
还是用迭代式信号传播算法,只是每个元件的计算逻辑分开写了。新增了引脚编号规则解析,以及三种不同的输出格式处理:普通门输出 "元件名 - 引脚号:值",译码器输出 "元件名:输出为 0 的引脚编号",数据分配器输出 "元件名:由 0 和 - 组成的字符串"
代码结构
// 三态门
static class TriStateGate{String name; TriStateGate(String n){this.name=n;}}
// 译码器
static class Decoder{String name; int inputCount; Decoder(String n,int c){this.name=n;this.inputCount=c;}}
// 数据选择器
static class Mux{String name; int controlCount; Mux(String n,int c){this.name=n;this.controlCount=c;}}
// 数据分配器
static class Demux{String name; int controlCount; Demux(String n,int c){this.name=n;this.controlCount=c;}}
static void calculateAll(){
for(Object gate : allGates){
if(gate instanceof TriStateGate){
TriStateGate g = (TriStateGate)gate;
if(signals.get(g.name+"-0") == 1){ // 控制端为1
signals.put(g.name+"-2", signals.get(g.name+"-1"));
}
}else if(gate instanceof Decoder){
Decoder g = (Decoder)gate;
if(signals.get(g.name+"-0") == 1 &&
signals.get(g.name+"-1") + signals.get(g.name+"-2") == 0){
int addr = 0;
for(int i=0;i<g.inputCount;i++){
if(signals.get(g.name+"-"+(3+i)) == 1){
addr |= 1 << i;
}
}
System.out.println(g.name + ":" + addr);
}
}
// 其他元件的计算逻辑...
}
}
复杂度分析


除了calculateAll方法写得非常乱,一堆instanceof和if-else堆在一起,我自己都不想看之外,其余方法都还算简洁
类图

时序图

bug 分析
强测直接扣了我 5 分,就因为数据分配器的输出顺序搞反了,我从引脚号最大的开始输出,结果字符串是倒过来的,互测中被 hack 了一个三态门的 bug,我一开始以为控制端为 0 时输出 0,结果应该是输出无效状态
不过互测的时候我也 hack 了别人好几个 bug,全是译码器引脚编号的问题
第三次作业:子电路和异常检测
我踩过的最大的坑
子电路输入输出搞反了!我以为子电路的输入是输入引脚,输出是输出引脚。结果正好相反:对于主电路来说,子电路的输入是输出引脚(主电路给子电路发信号),子电路的输出是输入引脚(子电路给主电路发信号)。当时子电路永远算不出输出,程序一直卡在那里。我把代码改了无数遍,都不知道问题出在哪。最后我在纸上画了一张主电路和子电路的信号流向图,才突然反应过来搞反了,把判断逻辑反过来之后,一下子就通了
还有一个坑是异常优先级搞反了,我先检查了 "没有输入" 再检查 "多个输入",结果样例 8 输出了错误的错误信息,强测直接扣了我 10 分,欲哭无泪
这次要做什么
在第二次作业的基础上,新增子电路定义与引用功能,以及五种异常输入检测。异常检测有严格的优先级,哪个错误先出现就先报哪个
我当时怎么想的
还是没考虑什么设计模式,用了最粗暴的方法:多 HashMap 存储子电路信息。每个子电路单独存它的输入、输出和连接信息,主电路用到的时候,给它单独开一块内存空间计算。异常检测就写了一个大方法,按照优先级顺序一个一个检查
核心逻辑
子电路独立计算:每个子电路有自己独立的信号空间,和主电路互不干扰。主电路把信号传给子电路的输入,等子电路算完了,再把它的输出拿回来给主电路用。子电路内部的元件输出时,在前面加上子电路编号前缀,避免和主电路重名
异常检测按照题目要求的优先级顺序:
一条连线里有两个以上的输入信号
一条连线里没有输入信号
一条连线里没有输出信号
输入和输出写反了
一个输入引脚接了两个不同的输出
只要发现了优先级高的异常,立刻输出错误信息并结束程序,后面的错误就不用管了
代码结构
// 存储子电路信息
static HashMap<String, ArrayList
static HashMap<String, ArrayList
static HashMap<String, ArrayList
// 全局信号和引脚映射
static HashMap<String, Integer> globalSignals = new HashMap<>();
static HashMap<String, String> pinMapping = new HashMap<>();
// 最终输出结果
static HashMap<String, Integer> finalResults = new HashMap<>();
// 判断一个引脚是输入还是输出
static int getPinType(String pin){
if(pin.startsWith("C")){
int idx = pin.indexOf('-');
String subId = pin.substring(1, idx);
String pinName = pin.substring(idx+1);
if(subInputs.get(subId).contains(pinName)){
return 2; // 子电路输入是主电路的输出
}
if(subOutputs.get(subId).contains(pinName)){
return 1; // 子电路输出是主电路的输入
}
}
int idx = pin.lastIndexOf('-');
if(idx != -1){
int pinNum = Integer.parseInt(pin.substring(idx+1));
return pinNum == 0 ? 1 : 2; // 普通引脚0是输出,其他是输入
}
return 1;
}
// 计算子电路
static void calculateSubCircuit(String subId, HashMap<String, Integer> inputSignals){
HashMap<String, Integer> subSignals = new HashMap<>(inputSignals);
// 子电路内部迭代计算
do{
updated = false;
updateSubPinValues(subSignals);
calculateSubGates(subSignals);
}while(updated);
// 把子电路输出放到全局信号里
for(String output : subOutputs.get(subId)){
if(subSignals.containsKey(output)){
globalSignals.put("C"+subId+"-"+output, subSignals.get(output));
}
}
}
// 计算主电路
static void calculateMainCircuit(){
do{
updated = false;
updateMainPinValues();
calculateMainGates();
}while(updated);
}
复杂度分析


这次作业的复杂度非常高,checkErrors、calculateSubCircuit和calculateMainCircuit三个方法复杂度都特别高,而且主电路和子电路的计算逻辑几乎完全一样,我相当于把同一段代码复制粘贴了一遍
类图

时序图

bug 分析
强测扣了 10 分,就是那个异常优先级搞反的问题。还有一个子电路名字冲突的 bug
设计反思
SOLID 原则分析
SRP 单一职责原则:基本满足。SubCircuit 类只负责封装子电路信息,InputParser 只负责解析输入,Main 类虽然东西多,但主要负责流程控制和计算。
OCP 开闭原则:完全不满足。三次作业下来,Main 类每次都要大改特改。新增元件要改计算方法,新增子电路要加子电路计算方法,新增异常要改错误检查方法。每次加新功能都要动核心代码,太痛苦了。
LSP 里氏替换原则:满足。因为我根本没写继承关系,所有类都是直接继承 Object,不存在子类替换父类的问题。
ISP 接口隔离原则:不满足。我根本没设计接口,所有依赖都是具体实现,扩展性极差。
DIP 依赖倒置原则:不满足。所有依赖都是具体类,没有依赖抽象。
改进方案
如果再给我一次机会,我会这么改:
用组合模式重构:设计一个Component接口,包含compute()方法。让BasicGate和SubCircuit都实现这个接口,这样主电路不需要区分是基本元件还是子电路,统一调用compute()方法就行。
用继承重构基本元件:写一个抽象父类AbstractGate,把所有元件共有的属性和方法放到父类里,每个具体元件只需要实现自己的compute()方法。这样就不用写一大堆instanceof判断了。
用责任链模式重构异常检测:给每种异常写一个单独的处理器,按优先级串成一条链。新增异常的时候,只要加一个新的处理器就行,不用改原来的代码。
用拓扑排序算法替换迭代式算法:现在的迭代式算法效率太低,对于大电路来说会很慢。拓扑排序只要扫一遍就能算出所有结果,效率高很多。
心得体会
这三次作业真的是我这学期写过最费劲也最有收获的代码。从最开始的 200 行到最后的 700 行,代码量翻了三倍多,我也从一个只会写简单小程序的小白,变成了一个能独立完成小型项目的开发者
这三次数字电路作业是我这学期最难忘的经历,从第一次半天写完的沾沾自喜,到第二次译码器 bug 熬到凌晨的崩溃,再到第三次异常优先级错了扣 10 分的心痛,我被现实狠狠打了脸
我终于明白课本上的 SOLID 原则不是空话,好的设计能省十倍的功夫;也终于懂了不能只信中测,边界条件和异常情况才是 bug 的重灾区
虽然过程痛苦,但我改掉了 "能跑就行" 的急功近利心态,学会了先设计再写代码、多测试再提交,这比代码本身更有价值
当然,我也认识到自己身上的很多问题。我太急功近利了,每次拿到题目就想着快点写完交上去,根本不愿意花时间去设计。我总是抱着 "先跑起来再说,后面有问题再改" 的心态,结果往往是后面改的时间比重新写一遍还要长。我也太粗心了,总是不仔细看题目,很多 bug 其实都是因为我漏看了题目里的一句话