Mondrian

导航

关于Java大作业答题判题程序1-3题的一个总结

一、前言

1.知识点

三次题目都主要涉及字符串相关的操作,只要我们熟悉字符串匹配、提取等操作,就可以得心应手了

(1)第一题主要涉及字符串的创建、找索引和提取以及分割,在Java当中,String类型的变量提供了许多很好用的方法,主要如下(以创建一个str为例来说明):

//字符串的创建
String str = "Hello, World!";
String str2 = new String("Hello, World!");
//字符查找
int index = str.indexOf('W');//查找字符的索引
int lastIndex = str.lastIndexOf('o');//查找最后一次出现的字符索引
//字符串分割
String[] parts = str.split(", ");//按照指定分隔符分割字符串
//去除空格
String trimmed = str.trim();//去除前后的空格

(2)第二题的字符串类型变化的还不是很多,所以所涉及的知识点并未有太多的变化,新涉及的知识点是计算字符串的长度以及做字符串的比较匹配:

//字符串长度
int length = str.length();//返回字符串的长度
//字符串比较
boolean isEqual = str.equals("Hello, World!");//比较内容
boolean isEqualIgnoreCase = str.equalsIgnoreCase("hello, world!");//忽略大小写比较

(3)第三题的字符串类型变化丰富,比较复杂,所以常规的equals方法已不适用该题目,需要用到正则表达式,在Java中,正则表达式(Regular Expressions,简称Regex)是一种用于描述字符串模式的工具。Java提供了java.util.regex包来支持正则表达式的使用,主要包含三个类:Pattern、Matcher和PatternSyntaxException。

Pattern类用于定义正则表达式的模式。你可以使用Pattern.compile()方法创建一个Pattern对象。

import java.util.regex.Pattern;
Pattern pattern = Pattern.compile("abc");

Matcher类用于执行匹配操作。你可以通过Pattern对象的matcher()方法来创建一个Matcher对象。

import java.util.regex.Matcher;
Matcher matcher = pattern.matcher("abcdef");
boolean found = matcher.find();//查找是否有匹配

2.难度

三道题的题目是循序渐进的,而且关联性很大,如果前面的没有设计好,后面的做起来要改的就会非常多

(1)第一次题目的内容基本都是按序输入,不涉及乱序,且开局会先输入题目的数量,所涉及的知识点主要是字符串的匹配与提取,以及对于个别测试点,有空格的处理操作。

(2)第二次题目的内容相较于第一次的题目有较大的改动,除了要进行字符串的匹配与提取之外,还涉及了乱序读入的方式,并且要合理的处理空字符的情况,并且如果第一次设计的类不合理的话,第二次代码的改动量将会变得非常大。

(3)第三次题目可以说必须要用正则表达式了,因为涉及到非法输入、多样字符串匹配,以及字符串格式多变的问题,例如空试卷和非空试卷都是合法,所以单一的使用equals()方法或indexOf()方法是很难满足所有测试点需求的。

二、设计与分析

1.第一次答题判题程序

通过读题我们发现,我们的录入信息并不多,只有“题目(#N)”和“答案(#A)”两种信息,所以只需要创建两个类“Question”和“AnswerSheet”就足够了,一个用来存储题目信息,另一个用来处理答卷信息。
类图如下:

总体的处理流程也比较简单,总体处理流程如下:

由于题目的格式是固定的,且第一次没有非法的输入(包括有多余的空格被视为合法题目),所以我们可以通过找固定的“#N:”、“#Q:”、“#A:”来完成题目编号、题目内容、答案信息的提取:

for (int i = 0; i < topic; i++)
{
     String que = input_data.nextLine();
     String str1 = que.substring(que.indexOf("#N:") + 3, que.indexOf("#Q:")).trim();
     String str2 = que.substring(que.indexOf("#Q:") + 3, que.indexOf("#A:")).trim();
     String str3 = que.substring(que.indexOf("#A:") + 3).trim();
     int num = Integer.parseInt(str1);
     questions[num - 1] = new Question(num, str2, str3);
}

由于答案的格式也是固定的,都遵循一个“#A:”后面跟一个答案,并且答案的数量和题目数量一致,所以我们直接可以通过分割“#A:”来提取答案信息:

String[] parts = answer.split("#A:");
String[] result = new String[parts.length - 1];
for(int i = 1; i < parts.length; i++)
    result[i - 1] = parts[i].trim();
for(int i = 0; i < result.length; i++)
    questions[i].setUserAnswer(result[i]);

2.第二次答题判题程序

相比于第一次答题判题程序,第二次的答题判题程序明显复杂了很多,首先是增加了试卷分数,每道题目都有相对应的得分,这就要求我们必须新增一个类来将试卷分数信息单独存储并处理,然后是三种信息可以混合输入,这就要求我们不能继续使用之前的顺序结构来“先处理题目信息再处理答卷信息”了,而是改用循环结构,对每一行未知的输入进行判断,然后再决定它要存储的位置,这使得数据的存储和构建更加具有了灵活性。
除了数据的新增外,这次题目还丰富了许多其他情况的说明,例如答案的数量可以和试卷当中的题目数量不相等,没有答案的题目计分为0,而多余的答案则直接忽略掉不做处理;
对于试卷总分不足100分的试卷,我们还要对此做一个输出提示,空的答案也需要做一个输出提示,所以这里需要我们来好好考虑该将这些细节问题放在什么位置来处理比较好,例如统计试卷总分这一项,我的做法是在每次录入试卷答案信息的方法里新增一个计分的动作,这样可以保证在录入信息的同时就能将试卷总分计算出来,而不用回头在写一个方法去统计。
除此之外,这道题目最关键的一点在于它可以拥有多张试卷和多张答卷,根据要求输出的情况来看,这里选择使用答卷来跟随试卷的处理方式比较好,首先是因为答卷属于输出处理项,我们对输出项的直接处理要比间接处理方便得多,其次是因为使用答卷去找试卷更加方便,可以在寻找之后直接输出结果,而不必设置一个flag从而在退出答卷的循环后再更具flag去判断是否需要打印“The test paper number does not exist”
类图如下:

总体处理流程如下:

由于题目的输入格式并未改变,只是新增了试卷信息和答卷信息的格式,并且所有测试点不存在非法输入,所以依旧沿用上一次的判断信息,对于每一行输入,只需要找到对应的关键字即可,然后根据标准的格式,找到子串的位置并提取,就能将所有的输入信息提取出来了,根据关键字判断信息的相关代码如下:

            if(str.indexOf("#T") != -1)
            {
                //......
                testPapers.add(testPaper);
                testPaperNum++;
            }
            else if(str.indexOf("#N")!= -1)
            {
                //......
                questions.add(question);
                questionNum++;
            }
            else if(str.indexOf("#S")!= -1)
            {
                //......
                answers.add(answer);
                answerNum++;
            }

对于“根据答卷对每一份卷子进行判题并加分输出”这一部分,使用了多层嵌套循环,目的是保障数据处理的层次的顺序性,即根据答卷信息来处理试卷信息,根据答卷找试卷,再根据试卷找题目,代码结构如下:

        for(int i=0; i<answerNum; i++)//answerNum为所统计的答题卡的数量
        {
            //从答题卡中找出卷子编号
            for(; testNum<testPaperNum; testNum++)//testPaperNum为试卷数量
            {
                //根据答题卡来找卷子
            }
            if(flag == 0) System.out.println("The test paper number does not exist");//没找到
            else//找到了
            {
                TestPaper testPaper = testPapers.get(testNum);
                for(int j=0; j<testPaper.getQNum(); j++)//在这张卷子上进行判题
                {
                    if(j >= aNum) System.out.println("answer is null");//没答案
                    else//有答案
                    {
                        int tempQuestionNum = testPaper.getQuestionNum(j);//获取题目编号
                        for(int k=0; k<questionNum; k++)//在题目列表里面寻找题目并做判断
                        {
                            Question question = questions.get(k);
                            if(question.getNumber() == tempQuestionNum)//找到题目了
                            {
                                //标准答案与用户答案进行比对
                                //根据答题信息进行加分
                            }
                        } 
                    }
                }
                for(int n=0;n<testPaper.getQNum();n++)
                {
                    //输出题目和答案信息
                    //输出得分信息
                }
            }           
        }

3.第三次答题判题程序

相比于前两次程序,第三次的数据格式更为混乱,不仅将输入的信息种类增加到了5种,并且允许对已输入的信息做修改(题目删除操作),而且还允许非格式化的输入,也就是说本次程序需要我们对输入的数据先进行一个判定,判断其是否符合标准格式。在此我们逐个分析:
题目信息的输入格式未变,所以我们可以先在上次的判断基础上增强格式的要求,来试试是否能满足本次题目的要求:

else if((str.indexOf("#N:")== 0)&&(str.indexOf("#Q:") > str.indexOf("#N:"))&&(str.indexOf("#A:") > str.indexOf("#Q:")))//此处处理学生信息题目信息

试卷信息的格式也未发生大变化,所以我们可以稍作修改,将原本的判断信息“出现#T:”限制为“#的位置必须是开头”,这样可以过滤诸如“1-2 #T:”这样的信息:

if(str.indexOf("#T:") == 0)//此处处理试卷信息

新增加的学生信息在题目里暂时只是起一个答题卡寻找答题人的作用,这里需要注意的是格式和试卷得分信息有所相似但不同,这里的信息格式“学号”和“姓名”之间为空格,每两个人的信息间有一个“-”:

else if(str.indexOf("#X:") == 0)//此处处理学生信息

对于答卷信息,格式里新增加了“学号”,所以在上次的信息处理上要做一些改动,对于信息的录入,改动和试卷一样:

else if((str.indexOf("#S:") == 0))//此处处理学生信息答卷信息

对于新增的“删除题目信息”,录入格式在试卷的判断方法的基础上,增加限定删除信息里的“-”要在“#D:”之后输出:

else if((str.indexOf("#D:") == 0)&&(str.indexOf("-") > 0))//此处处理删除信息

这道题的消息内容增加了许多,例如当试卷当中引用了不存在的题号时需输出“non-existent question~0”;
当题目存在但被“#D:”操作删除后,判分时要提示“the question 2 invalid~0”;
当“答案不存在、引用错误题号、题目被删除”三种情况同时存在时,又必须要输出“answer is null”,因为此类型的优先级最高。
所以在设计整体执行框架时,要搞清楚整体优先级,即判断的先后顺序。
类图如下:

程序主要执行的大体流程(基于第二次程序做的修改):

这道题目相比于前面两道题目,最大的特色是加入了“格式错误提示”,在做题的过程中,这个点占据了大部分的测试点,所以对于录入信息的判断是一件非常重要的事,按照先前的设计,经测试84分只能拿60分,所以经过研究后,还是使用正则表达式来对输入的信息进行判定,下为经测试后较为稳定的正则表达式:

String paperRegex = "#T:(\\d+)(?: \\d+-\\d+)*";//试卷信息的正则,用于匹配空试卷或包含多个答案的试卷(单独一个空格视为不合规范的输入)
String questionRegex = "#N:(\\d+) #Q:(.*) #A:(.*)";//题目信息的正则,基本格式定在这里,任何及任意长度的字符串都是题目、答案
String answerRegex = "#S:(\\d+) (\\d+)(?: #A:\\d+-.+)*";//答卷信息的正则,规定格式为必须要有编号、学号,若增加题号及答案信息,必须按照规定格式填写
String studentRegex = "#X:(\\d+ .+)*";//
String deleteRegex = "#D:N-(\\d+)";

因为程序代码越来越长,为了方便阅读及日后修改,所以将一部分功能从main中取出来,修改之后的main如图所示,流程一目了然:

    public static void main(String[] args)
    {
        List<Question> questions = new ArrayList<>();
        List<TestPaper> testPapers = new ArrayList<>();//创建试卷对象
        List<Answer> answers = new ArrayList<>();//创建答案对象
        List<Student> students = new ArrayList<>();//创建学生对象
        Count count = new Count();
        //解析解收到的信息,存好对应的地方
        receiveInformation(count, testPapers, questions, answers, students);
        //处理信息
        dealwithInformation(count.getTestPaperNum(), count.getAnswerNum(), count.getQuestionNum(), testPapers, questions, answers, students);
    }

三、踩坑心得

1.答题判题程序1

第一次的题目较简单,略微调试就可以通过全部测试点,唯一踩的坑可能就是没想到后面会迭代成乱序输入的,导致第一次的代码框架第二次就不能用了

2.答题判题程序2

这道题相比来说有点困难,但因为重新设计了代码框架的原因,大部分测试点调试起来都比较舒畅。
唯一出了问题的地方就是多张答卷那里,起先因为读题不仔细,认为还是一张试卷,所以最开始的答卷对象只创建了一个,新的答卷对象输入进来后会把旧的对象的信息给覆盖掉,例如这里的“两份答卷”样例:

#N:3 #Q:3+2= #A:5
#N:2 #Q:2+2= #A:4
#T:1 3-70 2-30
#S:1 #A:5 #A:22
#N:1 #Q:1+1= #A:2
#S:1 #A:5 #A:4
end

按理说应该会输出以下答案:

3+2=~5~true
2+2=~22~false
70 0~70
3+2=~5~true
2+2=~4~true
70 30~100

但由于我程序设计上的错误,只有答卷2的信息,没有之前答卷的信息,好在只是多添加一层循环的问题,改起来还是很方便的。

3.答题判题程序3

得益于第二次设计的合理的框架,所以这次题目改动和添加的地方并不多,但这道题是我目前为止踩坑最多的题目,主要集中在“错误的输入格式”这一点上,感觉它的测试点很扑朔迷离,但所幸最终还是全部调出来了。
(1)首先是未使用正则表达式时,能过60分的测试点,后面我在此基础上慢慢修改。
关于“空答案”,我当时将“空答案”理解为了“空格”,所以无论怎么调都是“格式错误”,后面问了同学才知道这里的“空答案”就是什么都没有,例如当测试点为:

#N:1 #Q:1+1= #A:
#T:1 1-5
#X:20201103 Tom
#S:1 20201103 #A:1- 
end

正确的结果应该是:

alert: full score of test paper1 is not 100 points
1+1=~~true
20201103 Tom: 5~5

即输入合法,并且“空”和“空”相匹配后还会输出“true”,该题得分。
但是当把“#S:1 20201103 #A:1- ”末尾的空格去除后,该条将会变为不合法的,即输出为:

wrong format:#S:1 20201103 #A:1-
alert: full score of test paper1 is not 100 points

所以说答卷末尾至少要对应一个字符,否则是不合法,而当字符为“ ”时又要对应为“空答案”,即什么都没有。
(2)再者就是“ansel is null”的优先级问题,当同时出现“答案不存在、引用错误题号、题目被删除”这三这时,“答案不存在”的优先级最高,这就使得我必须将这一层作为最外层的判断,然后再合理的设计内层循环条件,废了很长时间,但所幸最后还是修改好了。
(3)最后是困扰我很久的“错误的输入格式”这个问题,经过漫长的调试和试错后,我发现了格式的以下规律:
关于题目信息,题目内容和答案都可以不存在,但题目编号必须要存在,例如下面的题目是合法的:

#N:2 #Q: #A: 
#N:2 #Q:1+1= #A: 
#N:2 #Q:1+1= #A:2 

但下面的输入是不合法的:

#N: #Q: #A: 
#N: #Q:1+1= #A:2

关于试卷信息,只有两种输入是合法的,一种是只有“#T:+题目编号”(空试卷),但末尾不能加“ ”,否则不为合法;另一种就是完整的试卷信息,如下面所示:
合法的:

#T:1 1-1
#T:1//末尾不加空格

不合法的:

#T:1 //末尾有空格
#T:1 1- //加不加空格都不合法

关于学生信息,至少要有一个学生的信息(不是很确定,但目前没有专门关于学生信息的测试点),否则为不合法:
合法的:

#X:20201103 Tom-20201104 Jack

不合法的:

#X:Tom 20201103
#X:

关于答卷信息,也是和试卷一样,最少需要有一个答卷编号和学号,后面的题号和答案组合可以有0(空答卷)~很多组,且末尾也不能有“ ”
合法的:

#S:1 20201103 #A:1-5 #A:2-4
#S:1 20201103 #A:1- //末尾有空格
#S:1 20201103//末尾无空格

不合法的:

#S:1 20201103 //末尾有空格
#S:1 20201103 #A:1-5 #A:2-//末尾无空格
#S:1

关于删除题目信息,存在感不是很强(好像没有很多关于这个点的测试点),为了保险起见,严格按照题目中的格式来,总不能说有“空删除目标”这一说法吧,也就是说必须要有“#D:N-”这一串字符,并且结尾必须跟一个数字编号:
合法的:

#D:N-2

不合法的:

#D:N-//结尾无论有无空格都不合法
#D:

合法的:

#D:N-2

最后由于匹配机制的多样性,我放弃了原来的equals方法,转而使用正则表达式,用来适应字符串的多样性变化,效果甚是满意:

String paperRegex = "#T:(\\d+)(?: \\d+-\\d+)*";
String questionRegex = "#N:(\\d+) #Q:(.*) #A:(.*)";
String answerRegex = "#S:(\\d+) (\\d+)(?: #A:\\d+-.+)*";
String studentRegex = "#X:(\\d+) (.+)";
String deleteRegex = "#D:N-(\\d+)";

我的正则可能不是最标准的,但目前能适配这道题的所有测试点,如果还有错那基就是其它处理逻辑的错误了
通过修改正则表达式保证输入无误后,之前没过的测试点全部通过:

四、改进建议

第一次题目没什么好说的,第二次题目当时偷了个懒,很多处理的东西放在了main函数里,导致main内的东西很多,看上去非常不清楚流程,再加上当时没有写注释,个别变量名称也不是很规范,导致写第三题的时候非常痛苦,一边思考这里当初是怎么设计的,一边又得考虑这部分新内容放在哪个地方比较合理,而且main里的流程越写越长,看起来非常不舒服,后来只是简单的把两个主要流程(输入、输出)单独移出来写了个方法,这样main里看着舒服简洁多了,但还是不够优雅。我对我未来代码改进的建议就是:
(1)尽早规范出一套方法、变量的命名规范;
(2)养成写注释的习惯;
(3)过长的流程尽早封装为方法;
(4)降低代码耦合性,以便下次迭代还能用;
(5)在初期就设计框架时要考虑可扩展性,后期迭代后加新的内容方便加。

五、总结

我觉得这种大作业的形式做起来比较痛苦,但也比较爽,痛苦是因为很多测试点压根不知道该怎么处理,调试很痛苦,但当试了很久奇葩的测试点之后,你总能发现影藏问题的所在,然后把它解决之后,测试点一下就全过的时候,就会感觉到非常爽。这种题目更能锻炼我们的编码和设计能力,比如现在的大语言模型是无法一次性写几百行代码的,所以就强迫着我们自己动手去一步一步实现这个东西;锻炼设计能力是因为,这个题目是一次次迭代的,如果前期考虑的多,框架搭的足够好,可扩展性足够的强,那么在后期代码迭代的时候,处理起来会更加的得心应手,否则将要从头开始重新设计。
这几次题集让我学到了关于字符串处理的很多要点和细节,也让我认识到了“正则表达式”在复杂字符串匹配中的重要性,最主要的是锻炼了我设计、思考、解决问题的能力。

posted on 2024-10-26 20:49  克里斯琴  阅读(63)  评论(0)    收藏  举报