目录
前言
本次的三个作业,逐步构建了一个完整的答题判题程序,涵盖了题目信息的输入、试卷设计、答卷判断、错误处理等多个关键环节。本文将系统性总结这三次题目集的知识点、题量及难度,并分析其中的核心内容和实现方法。
1. 知识点总结
- 正则表达式:
以前我们通常是通过字符串处理函数来解决字符串问题,现在,面对复杂的文本情况,字符串函数无法应对,于是,我们使用正则表达式。每一道题目的输入解析过程结合了正则表达式的使用,使得复杂格式的数据能够有效识别。例如,在题目信息的解析中,正则表达式能够精准提取题目编号、内容及标准答案,为后续逻辑设置打下基础。
通过正则表达式的应用,处理多行输入、弹性题目编号及更多样化的格式,增强了程序的鲁棒性。 - 面向对象编程(OOP):
借助 OOP 的理念,设计了多个类(题目类、试卷类、答卷类等),将相关数据及方法封装于各自的类中:
题目类:负责存储题目编号、内容和标准答案,并提供方法来判断输入答案是否正确。
试卷类:管理试卷编号及试卷内题目的分值,确保评分系统的完整性。
答卷类:处理学生的答案和评分,将和试卷类中关联的数据相匹配。
这种结构化的设计提升了代码的可读性,便于后续的扩展与维护。 - 数据结构与算法设计:
有效使用了HashMap和Linklist等数据结构来存储和管理题目信息及答题结果,保证数据的快速检索与处理。
实现了算法来计算试卷总分,遍历题目答案并进行正确性验证,以确保评分准确性。
错误处理与异常管理:
系统设计中需要考虑到多种异常输入情况,如缺失试卷号、缺少答案等情况。响应这些情况的错误提示机制,例如"the test paper number does not exist"的输出,帮助用户理解输入错误。
2. 题量与样例
虽然只有三道题目,但是,每一道题目的难度比较大,而且是渐渐加大难度,需要实现封装的思想,还需要考虑系统的灵活性和适用性,方便迭代。
难点分析: - 多层次数据处理:必须同时处理题目、试卷和答卷信息的输入与解析,复杂性较高。
- 判题逻辑复杂性:在处理每一份答卷时,依据每道题目的标准答案与考生的答案进行匹配,要设计合理的逻辑。
- 错误和异常管理:要设计分支判断以及相应的提示信息,确保即使在输入错误时系统也能给予正确的反馈。
PTA第一次作业(7-5 答题判题程序-1)
设计与分析
题目分析
输入格式:
程序输入信息分三部分:
1、题目数量
2、题目内容
一行为一道题,可以输入多行数据。
格式:"#N:"+题号+" "+"#Q:"+题目内容+" "#A:"+标准答案
3、答题信息
答题信息按行输入,每一行为一组答案,每组答案包含第2部分所有题目的解题答案,答案的顺序号与题目题号相对应。
格式:"#A:"+答案内容
输出格式:
1、题目数量
格式:整数数值,若超过1位最高位不能为0,
2、答题信息
一行为一道题的答题信息,根据题目的数量输出多行数据。
格式:题目内容+" ~"+答案
3、判题信息
判题信息为一行数据,一条答题记录每个答案的判断结果,答案的先后顺序与题目题号相对应。
格式:判题结果+" "+判题结果
分析:通过分析题目,我认为需要设计三个class类,十分明显,首先我们要设计题目类,包括题目内容和标准答案,一个是答题信息类,包括所有题目的解题答案,但是,有多个题目,所以要设计一个包括所有题目类的类,我设计为试卷类,用于封装整套题目的信息。
通过分析格式,文本格式比较复杂,不方便用字符串处理函数来处理输入,通过观察,有特定的字符来代表特定的输入,比如“#N#Q#A”,于是我选择用正则表达式来处理输入。
通过分析内容,题目有题号,题目内容,标准答案,所以我使用HashMap<Integer, Question> questions,方便动态扩展,使用`ArrayList存储答题信息。
知识点解析
正则表达式:正则表达式是一种强大的文本模式匹配工具,它可以帮助我们在文本中查找、替换和提取特定模式的内容。Java 提供了丰富的正则表达式支持,通过 java.util.regex 包中的类和方法,我们可以在 Java 程序中使用正则表达式进行字符串处理。在调试过程中会详细给出。
ArrayList:ArrayList是Java集合框架的一部分,它是一种动态数组的数据结构,用于存储一维的有序元素序列。ArrayList的特点是可以动态地增加或减少容量,这使得它非常适合需要频繁修改大小的情况。以下是ArrayList的一些关键特性:
线程安全性:默认的ArrayList不是线程安全的,如果在多线程环境中使用,可能需要额外的同步措施。
随机访问:由于ArrayList是基于数组实现的,所以可以快速通过索引访问元素,时间复杂度为O(1)。
动态扩容:当添加的元素超过当前容量时,ArrayList会自动扩大容量并重新分配内存。
插入和删除效率:在列表头部和尾部操作(如add、remove等)相对高效,但如果在中间位置插入或删除元素,则需要移动后面的元素,时间复杂度为O(n)。
迭代器:提供Iterator接口,可以遍历ArrayList中的所有元素。
HashMap:HashMap是Java Collections Framework中的一个内置数据结构,它实现了键值对(Key-Value)映射,其中键(Key)和值(Value)都是对象。HashMap的主要特点是:
无序性:HashMap并不保证键值对的顺序,它的内部实现是哈希表,依赖于哈希函数将键转换为索引,所以每次插入、删除或查找的速度都很快,平均时间复杂度为O(1)。
碰撞处理:哈希函数可能会导致不同的键映射到相同的索引位置,这就是冲突。HashMap采用链地址法(Chaining)或开放寻址法(Open Addressing)解决冲突,即每个索引位置实际上是一个链表或桶,存储有相同的哈希值的键值对。
灵活性:键和值都可以是任意类型的对象,只要它们满足equals()和hashCode()方法的约定。
调试过程
1. 类的定义分析
首先,根据题目分析,设计三个类和一个Main类。Question 类封装了单个题目的信息,包括题号、题目内容和标准答案。函数方面提供了自定义构造方法来初始化题号、题目内容和标准答案,判题方法public boolean CheckAns(String answer)
比较给定答案与标准答案是否相同,重写toString方法public String toString()
,返回题目的详细信息。如图所示:
Exam 类用于管理一组题目,使用 HashMap 存储题目,以题号为键,Question 对象为值。主要是private HashMap<Integer, Question> questions
使用HashMap存储题目,键为题号,值为Question对象。使用默认构造方法,初始化空的题目列表和题目数量,还有get和set方法,如图所示:
AnswerSheet 类用于存储用户的答案及其判题结果,创建ArrayList来存储答案列表和判题结果列表
this.answers = new ArrayList<>(); // 初始化答案列表
this.results = new ArrayList<>(); // 初始化判题结果列表
然后用public void saveAnswer(String answer)
将用户的答案添加到列表中,最后再定义判题方法public void CheckAnss()
,根据用户的答案检查是否正确,通过public void PrintfAns()
输出题目和用户答案,如图所示:
Main 类中,
创建 Scanner 对象用于读取,建立 Exam 对象作为试卷的容器。
首先,读取题目数量,并使用循环读取每个题目的信息(题号、内容和标准答案)。
通过 exam.addQuestion() 方法将获取的题目添加到试卷中。
随后,创建 AnswerSheet 对象用于存储用户的答案,循环读取用户的答案,直到用户输入 “end” 为止。在此过程中若答案以 “A:” 开头,将其提取并保存。
最后,调用 CheckAnss() 方法进行判题,并调用 PrintfAns() 输出所有的题目、用户答案及判题结果。
完整类图如图所示:
2. 重点代码代码具体分析
上面介绍了类的定义,接下来,是重点代码代码具体分析,由于本次的输入复杂度不高,而且基本没有错误处理,所以,并没有完全使用正则表达式,更多的是通过Java String类中的split() 方法根据匹配给定的正则表达式来拆分字符串,然后通过特定字符信息来提取我们需要的信息。具体实现过程:
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
Exam exam = new Exam();
int questionCount = Integer.parseInt(scanner.nextLine());
for (int i = 0; i < questionCount; i++) {
String input = scanner.nextLine().trim();
String[] parts = input.split("#");
int number = Integer.parseInt(parts[1].split(":")[1].strip());
String content = parts[2].split(":")[1].strip();
String answer = parts[3].split(":")[1].strip();
exam.addQuestion(new Question(number, content, answer));
}
AnswerSheet answerSheet = new AnswerSheet(exam);
String answerInput;
while (!(answerInput = scanner.nextLine().trim()).equals("end")) {
String[] answers = answerInput.split("#");
for (String ans : answers) {
if (ans.startsWith("A:")) {
answerSheet.saveAnswer(ans.split(":")[1].strip());
}
}
}
answerSheet.CheckAnss();
answerSheet.PrintfAns();
}
}
int questionCount = Integer.parseInt(scanner.nextLine());
从控制台读取一行输入,表示题目的数量,并将其转换为整数 questionCount。
通过for 循环根据用户输入的题目数量,遍历并读取每一道题目的信息。
通过String input = scanner.nextLine().trim();
读取并去除用户输入行的前后空白字符。在调试的过程中,我是直接用String input = scanner.nextLine()
,但是一直测试用例一直没有通过,提示非零返回,通过调试,我用用trim()
方法,返回一个新的字符串,这个字符串将删除了原始字符串头部和尾部的空格。通过了测试用例。
根据输入样例的格式比如:样例:#N:1 #Q:1+1= #A:2,通过String[] parts = input.split("#");
将输入字符串按照 # 分隔成多个部分,用字符串数组parts存储。
parts[0] = "" // 第一个分割结果是空字符串,因为输入字符串从 # 开始
parts[1] = "N:1 " // 第二个分割结果,包含题号信息
parts[2] = "Q:1+1= " // 第三个分割结果,包含题目内容
parts[3] = "A:2" // 第四个分割结果,包含标准答案,易错点:分割后的数组包含空字符串 parts[0],需要注意在后续处理中去掉可能的空白字符,尤其是在提取信息时,例如使用 strip() 方法。
然后用同样的方法,调用split方法,int number = Integer.parseInt(parts[1].split(":")[1].strip());
,从分隔的字符串中提取题号,格式如 编号: 1,取出编号部分并通过Integer.parseInt()转换为整数,这个整数就是Question类的题号。在这里,要注意使用.strip():对冒号后面提取出来的字符串进行去除两端空白字符的操作,为了去除可能存在的空格、制表符等。
同样的方法,String content = parts[2].split(":")[1].strip();
提取Question类的内容。
String answer = parts[3].split(":")[1].strip();
提取Question类的标准答案。
然后,exam.addQuestion(new Question(number, content, answer));
创建一个 Question 对象,使用exam.addQuestion方法将其添加到 Exam 对象中。
AnswerSheet answerSheet = new AnswerSheet(exam);
创建 AnswerSheet 对象,用于存储用户的答案并与 Exam 对象关联。
String answerInput;
声明一个字符串变量用于存储用户的输入答案。
while (!(answerInput = scanner.nextLine().trim()).equals("end"))
;当用户输入的内容不是 "end" 时循环读取用户答案。输入 "end" 会结束循环。
String[] answers = answerInput.split("#");
和上面的方法一样,将用户输入的答案按 # 分隔成多个部分,以便逐个处理答案。
for (String ans : answers)
遍历用户每一个答案。
if (ans.startsWith("A:"))
检查答案是否以 "A:" 开头,确保该字符串是一个有效的答案输入。
answerSheet.saveAnswer(ans.split(":")[1].strip());
提取答案部分,并将其保存到AnswerSheet 对象中。
answerSheet.CheckAnss();
调用 CheckAnss 方法进行判题,检查用户提交的答案是否正确。
answerSheet.PrintfAns();
调用 PrintfAns 方法输出每道题目的内容、用户的答案及该答案的判定结果。
时序图,耦合度
改进建议
1.当前代码没有对用户输入进行足够的验证。还要添加一些输入检查,确保输入格式正确,避免因格式错误导致的异常。比如在读取题号、内容和标准答案时,检查 parts 数组的大小,确保切分后的数组有足够的元素。
2.Main类中,字符串处理操作代码冗长,可以提取出一些方法来提高可读性。
3.CheckAnss 方法只是将结果存储在一个列表中。可以考虑在判题时输出每道题的结果,增强用户反馈。
PTA第二次作业
设计与分析
题目分析
输入格式:
程序输入信息分三种,三种信息可能会打乱顺序混合输入:
1、题目信息
一行为一道题,可输入多行数据(多道题)。
格式:"#N:"+题目编号+" "+"#Q:"+题目内容+" "#A:"+标准答案
2、试卷信息
一行为一张试卷,可输入多行数据(多张卷)。
格式:"#T:"+试卷号+" "+题目编号+"-"+题目分值
3、答卷信息
答卷信息按行输入,每一行为一张答卷的答案,每组答案包含某个试卷信息中的题目的解题答案,答案的顺序与试卷信息中的题目顺序相对应。
格式:"#S:"+试卷号+" "+"#A:"+答案内容
答题信息以一行"end"标记结束,"end"之后的信息忽略。
输出格式:
1、试卷总分警示
2、答卷信息
一行为一道题的答题信息,根据试卷的题目的数量输出多行数据。
格式:题目内容+""+答案++""+判题结果(true/false)
3、判分信息
判分信息为一行数据,是一条答题记录所对应试卷的每道小题的计分以及总分,计分输出的先后顺序与题目题号相对应。
格式:题目得分+" "+....+题目得分+"~"+总
4、提示错误的试卷号
如果答案信息中试卷的编号找不到,则输出”the test paper number does not exist”,
分析:通过题目分析,在题目1的基础上,增加了试卷信息,格式:"#T:"+试卷号+" "+题目编号+"-"+题目分值,包括试卷号,题目编号,题目分值。由于我在作业1中已经设计了设计三个class类,其中为试卷类用于封装整套题目的信息。所以,只需要在作业1的基础上进行细节的修改。通过分析输入格式,三种信息可能会打乱顺序混合输入,输入的信息格式也不相同,还有不同的格式约束,比如答案数量可以不等于试卷信息中题目的数量,没有答案的题目计0分,多余的答案直接忽略,还有要各种各样的错误处理,比如如果输入的答案信息少于试卷的题目数量,没有答案信息的题目输"answer is null" ,提示错误的试卷号如果答案信息中试卷的编号找不到,则输出”the test paper number does not exist”,所以,我选择使用正则表达式提取信息,然后通过if的嵌套判断来处理复杂的格式约束和错误处理。
知识点解析(不重复)
LinkedHashMap是 Java 中的一个类,它继承自HashMap。它在HashMap的基础上维护了一个双向链表,这个链表定义了迭代顺序,使得它既拥有HashMap快速查找的特性,又能按照插入顺序或者访问顺序(可以通过构造函数配置)来遍历元素。常用方法
put 方法:用于插入键值对。get 方法:用于获取指定键对应的元素。remove 方法:用于移除指定键对应的键值对。keySet方法:返回一个包含所有键的Set集合。
java.util.regex 包:java.util.regex 包是 Java 标准库中用于支持正则表达式操作的包。java.util.regex 包主要包括以下三个类:
Pattern 类:pattern 对象是一个正则表达式的编译表示。Pattern 类没有公共构造方法。要创建一个 Pattern 对象,你必须首先调用其公共静态编译方法,它返回一个 Pattern 对象。该方法接受一个正则表达式作为它的第一个参数。
Matcher 类:Matcher 对象是对输入字符串进行解释和匹配操作的引擎。与Pattern 类一样,Matcher 也没有公共构造方法。你需要调用 Pattern 对象的 matcher 方法来获得一个 Matcher 对象。
PatternSyntaxException:PatternSyntaxException 是一个非强制异常类,它表示一个正则表达式模式中的语法错误。
调试过程
1.类的定义分析
1.Question 类用于表示一个问题,包括其内容和标准答案。构造函数有无参构造函数和带参构造函数。定义了方法:
public String getContent():返回问题的内容。
public String getStandardAnswer():返回问题的标准答案。
public boolean isCorrectAnswer(String answer):接受一个答案并与 standardAnswer 进行比较,相同返回 true,否则返回 false。
2. TestPaper 类表示一份试卷,包含多个问题以及它们的分值和试卷的总分。
属性包括试卷中问题的总数,默认为0。使用 LinkedHashMap问题的 ID(题号)和对应的分数,保证插入顺序。fullscore表示试卷的满分,默认为0。
定义了public Map<Integer, Integer> getquestions()来返回试卷中的问题(不可修改的 Map)。
public void addQuestion(int qid, int score)向试卷中添加问题,接收问题 ID 和分数作为参数;同时更新问题数量和满分。
3. Answer 类用于表示用户的答案,记录每个答案及其相应的评析结果。
属性包括答案的 ID,答案的数量,存储用户的答案列表ArrayList,存储每个答案的正确性的列表。
函数有初始化答案列表、答案数量和答案 ID函数,get和set函数,public void setresults(boolean result),public List
完整类图:
2.重点代码代码具体分析
先是初始化定义,
LinkedHashMap<Integer, Question> question = new LinkedHashMap<>(); // 存储问题(问题编号到问题对象)
LinkedHashMap<Integer, TestPaper> testpaper = new LinkedHashMap<>(); // 存储试卷(试卷编号到试卷对象)
LinkedList<Answer> ans = new LinkedList<>(); // 存储用户的答案对象的列表
然后程序进入一个无限循环处理用户输入,直到用户输入 "end"。
接下来,进入正则表达式处理部分,设置String sample = input.nextLine();
,sample为一行输入的字符。通过正则表达式匹配sample达到提取信息的目的,
if (sample.startsWith("#N:"))
检查所读取的 sample 输入字符串是否以 "#N:" 开头,来判断输入是关于一个问题的定义,从而在接下来提取提取出问题的编号、内容和标准答案。其中startsWith 是 Java 中 String 类的一个方法,用于检查字符串是否以指定的前缀开始。
调用indexOf方法,indexOf 方法返回一个整数值,指出 String 对象内子字符串的开始位置。如果没有找到子字符串,则返回-1,sample.indexOf("#N:"); sample.indexOf("#Q:"); sample.indexOf("#A:"); 分别找到 "#N:""#Q:""#A:" 的索引位置,然后,使用 substring(index1 + 3, index2) 提取出 #N: 到 #Q: 之间的字符,即表示问题编号的部分,一定要用trim() 方法去除首尾的空格,确保提取的编号是整洁的字符串。使用 substring(index2 + 3, index3) 提取 #Q: 到 #A: 之间的字符,即问题内容部分,使用 substring(index3 + 3, newin.length()) 提取 #A: 后的所有内容,即标准答案的部分。
String num = sample.substring(sample.indexOf("#N:") + 3, sample.indexOf("#Q:")).trim(); // 获取问题编号,并去除前后空格
int number = Integer.parseInt(num); // 将问题编号转换为整数
String content = sample.substring(sample.indexOf("#Q:") + 3, sample.indexOf("#A:")).trim(); // 获取问题内容
String standardAnswer = sample.substring(sample.indexOf("#A:") + 3, sample.length()).trim(); // 获取标准答案
question.put(number, new Question(content, standardAnswer)); // 将问题存入问题集合
再将提取的问题的编号、内容和标准答案存入问题集合。
if (sample.startsWith("#T:"))
检查sample输入是否以 "#T:" 开头,表明这是不是有关试卷的信息。如果是,那么后续代码将处理该信息。 接下来进入正则表达式部分。
Pattern pattern1 = Pattern.compile("#T:\\s*(\\d+)\\s*(.*)"); // 编译正则表达式
Matcher matcher1 = pattern1.matcher(sample); // 创建匹配器
创建一个正则表达式模式pattern1,来匹配试卷信息。含义如下:#T::匹配字符串 #T:。\s:匹配任意数量的空白字符(包括空格和制表符)。(\d+):匹配一个或多个数字,并捕获这些数字(这是试卷编号)。\s:继续匹配空白字符。(.*):匹配随后的所有字符,并捕获(试卷的其他描述信息)。
创建匹配器matcher1使用 sample 字符串对该模式进行匹配。
查找匹配内容:调用 matcher1.find() 寻找第一个匹配项,int pid = Integer.parseInt(matcher1.group(1).trim());
通过捕获组group(1)(\d+)提取试卷编号并转换为整数pid。
Pattern pattern2 = Pattern.compile("(\\d*)-(\\d*)"); // 定义正则表达式匹配问题ID和分数
Matcher matcher2 = pattern2.matcher(sample); // 创建匹配器
创建另一个正则表达式模式pattern2,用于匹配问题 ID 和分数。\d:匹配零个或多个数字。-:匹配连接问题 ID 和分数的破折号。\d:匹配分数。
创建匹配器matcher2将该模式应用于sample字符串,以便接下来的遍历匹配。用 while 循环来查找并处理所有符合正则表达式的匹配项,通过matcher2.group(1)提取匹配的第一个组(问题 ID),并将其转换为整数 qid。通过matcher2.group(2)提取匹配的第二个组(分数),并将其转换为整数 score。调用 TestPaper 类中的 addQuestion 方法,最后,将提取到的问题 ID 和分数添加到当前的 paper 对象中,将试卷对象 paper 以试卷编号 pid 作为键存储到 testpaper 集合中。这样就完成了试卷信息的存储。
if (sample.startsWith("#T:")) {
TestPaper paper = new TestPaper(); // 创建新的试卷对象
Pattern pattern1 = Pattern.compile("#T:\\s*(\\d+)\\s*(.*)"); // 编译正则表达式
Matcher matcher1 = pattern1.matcher(sample); // 创建匹配器
matcher1.find(); // 查找匹配的内容
int pid = Integer.parseInt(matcher1.group(1).trim()); // 提取试卷编号并转换为整数
Pattern pattern2 = Pattern.compile("(\\d*)-(\\d*)"); // 定义正则表达式匹配问题ID和分数
Matcher matcher2 = pattern2.matcher(sample); // 创建匹配器
while (matcher2.find()) { // 遍历匹配的结果
int qid = Integer.parseInt(matcher2.group(1)); // 提取问题ID
int score = Integer.parseInt(matcher2.group(2)); // 提取分数
paper.addQuestion(qid, score); // 将问题添加到试卷中
}
testpaper.put(pid, paper); // 将试卷存入试卷集合
}
接下来对于答案部分"#S:"的处理逻辑也是相同的。代码首先检查输入是否对应于答案部分,然后提取出答案 ID 和多个答案。通过正则表达式和字符串操作,代码获取答案并填补缺失的答案,最后将所有答案信息封装到 Answer 对象中,便于后续处理。
Pattern pattern3 = Pattern.compile("#S:\\s*(\\d*)\\s*(#A:(.*))*"); // 编译正则表达式
Matcher matcher3 = pattern3.matcher(sample); // 创建匹配器,用于在 sample 中查找匹配
Pattern pattern4 = Pattern.compile("#A:"); // 定义一个新的正则表达式,用于查找答案标记 "#A:"
Matcher matcher4 = pattern4.matcher(sample); // 创建匹配器,用于在 sample 中查找答案标记
通过上面对于输入信息的处理提取,接下来要输出信息,包括格式约束和错误处理的调整优化。
- 验证试卷的满分:使用 keySet() 方法获取所有试卷的 ID,并对每个 ID 进行遍历。对于每个试卷,通过 getfullscore() 方法获取满分并检查是否为100分。如果发现某试卷的满分不是100分,则输出警告信息。
- 验证答案的正确性
先遍历答案,并获取其对应的试卷 ID,然后确认 testpaper 中是否包含这个 ID,如果不包含,则输出错误提示并跳过该答案对象的后续处理。 - 检查答案的内容
设置一个 INDEX 变量用于跟踪当前的答案索引,通过试卷 ID 获取与之对应的所有问题,使用 keySet() 方法遍历每个问题的 ID。在 ANS 对象中存储每个答案与其标准答案的比较结果(正确与否)。 - 检查答案数量和输出
如果答案数量少于试卷中的题目数量分为没有答案和至少有一个答案可能,进行不同的处理。 - 计算及输出总分
重置索引和初始化总分,然后遍历每道问题以计算分数,对每个问题判断答案的正确性。
根据正确答案打印相应的分数(如果是第一个正确答案则直接输出;否则在前面加空格)。
将每个正确答案的分数累加到 all。对于错误的答案,打印0分;同样处理第一个和后续错误的输出格式。在处理完所有问题后,在一行中输出总分,格式为 "~" + total_score。
简要流程如下:
验证每道试卷的满分是否为100分。
遍历每个答案对象,确保试卷存在,并检查每个答案的正确性。
根据每道题的标准答案和提交的答案,输出每道题的得分情况。
如果答案数量少于题目数量,特别处理未回答的问题,并输出相应的信息。
最后,计算和打印出每个试卷的总分。
类图,耦合度
改进建议
主函数里面逻辑处理太多,不方便进行迭代,应该再划分出几个类来处理格式约束,错误处理,输出等功能。
PTA第三次作业
设计与分析
题目分析
输入格式:
1、题目信息
题目信息为独行输入,一行为一道题,多道题可分多行输入。
格式:"#N:"+题目编号+" "+"#Q:"+题目内容+" "#A:"+标准答案
2、试卷信息
试卷信息为独行输入,一行为一张试卷,多张卷可分多行输入数据。
格式:"#T:"+试卷号+" "+题目编号+"-"+题目分值+" "+题目编号+"-"+题目分值+...
3、学生信息
学生信息只输入一行,一行中包括所有学生的信息,每个学生的信息包括学号和姓名,格式如下。
格式:"#X:"+学号+" "+姓名+"-"+学号+" "+姓名....+"-"+学号+" "+姓名
4、答卷信息
答卷信息按行输入,每一行为一张答卷的答案,每组答案包含某个试卷信息中的题目的解题答案,答案的顺序号与试 卷信息中的题目顺序相对应。答卷中:
格式:"#S:"+试卷号+" "+学号+" "+"#A:"+试卷题目的顺序号+"-"+答案内容+...
5、删除题目信息
删除题目信息为独行输入,每一行为一条删除信息,多条删除信息可分多行输入。该信息用于删除一道题目信息,题目被删除之后,引用该题目的试卷依然有效,但被删除的题目将以0分计,同时在输出答案时,题目内容与答案改为一条失效提示,例如:”the question 2 invalid~0”
格式:"#D:N-"+题目号
输出格式:
1、试卷总分警示
2、答卷信息
3、判分信息
4、被删除的题目提示信息
5、题目引用错误提示信息
6、格式错误提示信息
7、试卷号引用错误提示输出
8、学号引用错误提示信息
分析:作业3相对于作业2在类方面增加了学生信息和删除题目信息,还有更复杂的处理逻辑和更多的异常情况和更繁琐的格式约束,极大的考验我们代码的逻辑性和健壮性。对于我来说是一个难题。
知识点解析
调试过程
1.类的定义分析
Question类属性包括题目的唯一标识,题目的具体内容描述,该题目的标准答案和isvaildOfQue用于处理题目有效性的判断。方法包括构造方法、get和set方法。
TestPaper类属性包括试卷中题目的数量,LinkedHashMap存储题目编号和对应的分数,和试卷的总分。方法包括构造方法,get和set方法、试卷中题目编号与分数的映射setQuestions(LinkedHashMap<Integer, Integer> questions)、addQuestion(int questionID, int score):向试卷中添加一道新题目,更新题目数量和总分,并将题目编号和分数存入映射中方法。
Answer类属性包括答案的唯一标识符、答案的数量、学生的唯一标识符、LinkedHashMa存储题目编号和对应的答案内容、一个List存储每个答案的结果(正确与否)。
方法包括构造方法、get和set方法。
Student类包括学生的唯一标识符、学生的姓名。
方法包括构造方法、get和set方法。
Input类包括处理输入的正则表达式类的方法。具体有
private static void processInput(Scanner input, LinkedHashMap<Integer, Question> questionMap, LinkedHashMap<Integer, TestPaper> testpaperMap, LinkedList
private static void handleInvalidQuestionInput(String entry, LinkedHashMap<Integer, Question> questionMap)
private static void handleStudentInput(String entry, LinkedList
private static void handleAnswerInput(String entry, LinkedHashMap<Integer, TestPaper> testpaperMap, LinkedList
Checkif类包括判断处理逻辑和处理异常情况格式约束,每种输入格式需要验证其符合性,不符合时应记录错误信息,还要照输入顺序输出错误信息、答题结果和评分信息。
总体类图:
2.重点代码代码具体分析
Input类输入处理部分:
试卷输入(“#T:” 开头):用正则表达式检查格式,要求以 “#T:” 开头,后跟整数试卷编号和多个 “问题编号 - 分数” 组合。若格式正确,提取编号创建试卷对象,再用另一正则提取每个问题编号和分数,添加到试卷对象并存入映射。
无效问题标记输入(“#D:N-” 开头):正则检查格式,以 “#D:N-” 开头后跟整数问题编号和空格。若正确,提取编号,若问题映射中有该编号则标记问题无效。
问题输入(“#N:” 开头):正则检查格式,以 “#N:” 开头后跟问题编号、“#Q:” 问题内容、“#A:” 标准答案。正确则提取信息创建问题对象存入映射。
学生输入(“#X:” 开头):正则检查格式,以 “#X:” 开头后跟学生编号和名字及多个 “- 学生编号 学生名字” 组合。用另一正则提取学生编号和名字,创建学生对象加入列表。
答案输入(“#S:” 开头):正则检查格式,以 “#S:” 开头后跟试卷编号、学生编号和名字及多个 “#A: 答案序号 - 答案内容” 组合。正确则提取编号,若试卷映射中有该编号,用另一正则提取答案序号和内容,存入答案映射并创建答案对象加入列表。
Pattern pattern1 = Pattern.compile("(\\d+)-(\\d+)"); // 匹配问题和分数的正则表达式
Matcher matcher1 = pattern1.matcher(entry); // 创建第二个匹配器
Pattern pattern2 = Pattern.compile(regex); // 编译正则表达式 //使用 Pattern.compile() 将正则表达式编译成模式对象 pattern2
Matcher matcher2 = pattern2.matcher(entry); // 创建匹配器 //创建 Matcher 对象 matcher2 来匹配输入字符串 entry
Pattern pattern3 = Pattern.compile("#A:(\\d+)-?([^#]*)"); // 定义匹配答案的正则表达式
Matcher matcher3 = pattern3.matcher(entry); // 创建匹配器
Pattern pattern4 = Pattern.compile("#X:(\\s*(\\d+)\\s*(.*)(-\\s*(\\d+)\\s*(.*))*)"); // 定义匹配学生信息的正则表达式
Matcher matcher4 = pattern4.matcher(entry); // 创建一个 Matcher 对象 matcher4,用于在输入字符串中查找匹配
Pattern pattern5 = Pattern.compile("\\s*(\\d+)\\s*(\\w+)\\-*"); // 定义正则表达式以匹配学生ID和名字
Matcher matcher5 = pattern5.matcher(entry); // 创建匹配器
Pattern pattern6 = Pattern.compile("#D:N-\\s*(\\d+)\\s*"); // 定义匹配无效问题的正则表达式
Matcher matcher6 = pattern6.matcher(entry); // 创建匹配器
Checkif输出部分
遍历试卷映射中的每个试卷,检查其总分是否为 100。如果不是,打印警告信息。
然后处理答案,遍历答案列表中的每个答案对象。
首先获取答案的试卷编号,检查试卷映射中是否存在该编号。如果存在,继续处理。
创建一个学生对象,遍历学生列表,检查学生编号是否与答案中的学生编号匹配。如果匹配,设置标志为 1,并将找到的学生对象赋值给变量。
初始化一个题号变量,遍历试卷中的问题。对于每个问题,检查答案中是否包含该问题的答案。如果包含且问题存在于问题映射中,比较答案与标准答案,设置答案的结果为正确或错误。如果答案中不包含该问题的答案,设置结果为错误。
初始化总分变量,根据答案计数是否为 0 进行不同的处理。
如果答案计数不为 0,遍历试卷中的所有问题,检查答案中是否包含该问题的答案。如果包含且问题存在于问题映射中,检查问题是否有效。如果有效,打印问题内容、学生答案及其是否正确。如果问题不存在或无效,打印相应的信息。如果找到学生,打印学生的 ID 和姓名,遍历试卷中的每个问题,根据答案的正确性输出分数或 0,并累加总分。最后打印总分。如果未找到学生,打印学生未找到的信息。
如果答案计数为 0,处理方式类似,但每个问题的得分为 0。如果找到学生,打印学生的 ID 和姓名以及总分为 0。如果未找到学生,打印学生未找到的信息。
如果试卷不存在,打印试卷不存在的信息,并继续下一个答案。
类图,耦合度
改进建议
代码的数据结构还可以进一步优化,输出部分可以尝试用专门的异常处理函数来处理,降低逻辑复杂度,减少if无用的嵌套。后面还需要增加其他异常情况的处理。
踩坑心得
- 多种输入格式的复杂性
代码需要处理多种不同格式的输入,包括问题(#N:)、试卷(#T:)、无效问题标记(#D:N-)、学生信息(#X:)和答案(#S:),以及结束标记(end)。每种输入格式都有其特定的规则和要求,这增加了代码理解和处理的难度。
例如,对于试卷输入格式#T:\s(\d)\s(\s\d+-\d+\s),需要理解正则表达式中各个部分的含义,如\s表示允许空格的存在,(\d)用于匹配试卷编号,(\s\d+-\d+\s)*则匹配题目编号和分数的组合。由于题目格式不是完全固定,有各种各样的输入异常,我没有完全掌握正则表达式,在字符匹配一直出现错误。导致作业2一直没有AK。
-
格式验证的严谨性
代码中对输入格式的验证非常严格,通过正则表达式进行匹配。由于我对于空字符没有理解,没有注意到,测试用例一直没有通过,后面查资料,在处理输入字符中,使用trim()方法,(返回一个新的字符串,这个字符串将删除了原始字符串头部和尾部的空格)和使用 strip() 方法,(尤其是在提取信息时)(分割后的数组包含空字符串 parts[0],需要注意在后续处理中去掉可能的空白字符)。 -
输入处理的逻辑顺序
代码在处理不同类型的输入时,需要通过if-else语句进行判断和处理,逻辑顺序较为清晰。然而,由于我没有完全读懂题目的意思,当输入类型较多时,没有仔细跟踪每个分支的处理逻辑,后面通过画逻辑图,成功通过。例如,在处理#S:格式的学生答案输入时,首先要验证格式是否正确,然后提取考卷 ID、学生 ID、答案序号和内容等信息,再进行后续的处理。如果在这个过程中忽略了某个步骤或者处理顺序错误,可能会导致数据提取不准确或后续计算出现问题。
还有我对于多种数据结构的使用,数据一致性和完整性、异常情况处理等。总体而言,这段代码涵盖了较为复杂的业务逻辑和多种数据结构的操作,在理解和使用时需要仔细分析每个部分的功能和逻辑,注意输入格式的正确性、数据的一致性和完整性,以及业务逻辑的准确性。同时,对于可能出现的异常情况,需要进一步思考和完善处理机制,以确保程序能够稳定、正确地运行。通过对这些 “坑” 的认识和理解,可以更好地优化和维护代码,提高开发效率和程序质量。
总结
通过这次的PTA大作业,首先,我收获了对正则表达式更深入的理解和应用能力。代码中频繁使用正则表达式来验证和解析各种输入格式,如试卷格式、学生信息格式、答案格式等。通过学习这些正则表达式的编写,我学会了如何精确地匹配特定的字符串模式,提取关键信息。例如,在处理试卷输入时,#T:\s(\d)\s(\s\d+-\d+\s)这个正则表达式巧妙地匹配了试卷编号以及题目编号和分数的组合,让我明白了如何利用正则表达式的分组功能来获取所需的具体数据。这在处理复杂的文本输入和格式验证场景中非常实用,能够高效地筛选和处理符合特定规则的数据。代码中根据数据的特点和操作需求,需要合理地选择了LinkedHashMap、LinkedList和ArrayList等数据结构,我深刻体会到了不同数据结构在不同场景下的优势。此外,还需要仔细设计if-else分支结构,确保每种输入类型都能得到正确的处理。同时,对于输入格式的验证和错误处理也非常重要,这让我学会了在程序中如何保证输入数据的合法性和可靠性,避免因错误的输入导致程序异常或结果错误。最后,代码中采用了面向对象的编程方式,让我
标签:题目,JAVAPTA,信息,卢翔,答案,22207112,格式,试卷,输入 From: https://www.cnblogs.com/LX14332234/p/18503911