一、前言
總結三次題目集的知識點、題量、難度等情況
這三次題目集的最後一道大作業是一系列遞進的程式設計作業。
第一次作業 答題判題程式-1
設計一個簡單的答題判題程式,要求輸入題目資訊和答題資訊,根據標準答案判斷答題結果。
主要知識點:
- 基本的類設計和物件建立。
- 輸入輸出處理。
- 字串處理。
- 正規表示式。
第二次作業 答題判題程式-2
在第一次作業的基礎上,增加了試卷資訊和題目分值,要求處理更復雜的輸入資料,並根據題目分值計算總分。
主要知識點:
- 擴充套件的類設計,包括試卷類和題目類的關聯。
- 資料結構的應用,如列表和字典,用於儲存和管理題目和試卷資訊。
- 演算法實現,包括排序等。
第三次作業 答題判題程式-3
進一步擴充套件程式,增加了學生資訊、刪除題目資訊和答題資訊的處理。要求處理更復雜的輸入資料,包括學生資訊、試卷資訊、題目資訊和答題資訊,並實現更復雜的邏輯。
主要知識點:
- 高階類設計,包括學生類、試卷類、題目類和答題類的設計。
- 資料結構的應用,如列表、字典和集合,用於儲存和管理複雜的資料。
- 演算法實現,包括排序、搜尋和分數計算。
- 系統設計,包括程式結構和模組化設計。
題量:每次作業都逐漸增加,從簡單的題目資訊和答題資訊處理,到複雜的試卷資訊、學生資訊和刪除題目資訊的處理。
難度:隨著題目的遞進,難度也逐漸增加,要求掌握更復雜的類設計、資料結構應用和演算法實現。
二、設計與分析
重點對題目的提交原始碼進行分析,可參考SourceMontor的生成報表內容以及PowerDesigner的相應類圖,要有相應的解釋和心得(做到有圖有真相),本次Blog必須分析題目集1~3的最後一題
第一次作業 答題判題程式-1
設計類圖如下:
第一次PTA大作業難度較低,但由於這個時候我對於類的建立不是很熟悉,只想著去實現它的全部功能就好,也沒有對類做過多的分析和考慮,所以這裡只建立了一個Question類,而把大部分邏輯程式碼全部放在了Main函式里面。
程式邏輯:
- 初始化輸入:程式開始時,讀取使用者輸入的問題數量。
- 建立資料結構:根據問題數量,初始化用於儲存問題物件和答案結果的陣列。
- 讀取題目資訊:透過函式讀取使用者輸入的每個問題詳細資訊,包括編號、內容和標準答案。
- 讀取使用者答案:透過函式讀取使用者對每個問題的回答。
- 檢查結束標誌:確認是否收到結束輸入,如果不是"end",則終止程式。
- 比對答案:透過函式比對使用者答案和標準答案,記錄每個問題的回答是否正確。
- 輸出結果:輸出每個問題的題目內容和使用者的答案,以及使用者回答的正確與否。
詳細設計:
Question類:
- 屬性:id題目編號、content題目內容和standardAnswer題目標準答案。
- 方法:
建構函式:初始化題目的編號、內容和標準答案。
getId():返回題目的編號。
getContent():返回題目的內容。
getStandardAnswer():返回題目的標準答案。
equals(Question question):判斷兩個題目是否相同,依據是它們的編號是否相等。
Main類:
- 方法:主方法控制程式整體流程,其他方法分別用於讀取題目資訊、讀取使用者答案、檢查結束標誌、比對答案和輸出結果。
第二次作業 答題判題程式-2
設計類圖如下:
相較於第一次大作業,這一次對題目進行了重新的規劃,設計了Question、TextParser、AnswerSheet、Exam四個類。
在這個類圖中:
Main
類使用TestParser
類來解析輸入。TestParser
類建立Question
、Exam
和AnswerSheet
物件。Exam
類包含Question
物件。AnswerSheet
類使用Exam
類和Question
類來計算得分和輸出結果。
主要邏輯和詳細設計:
1. 初始化和輸入解析
- 使用
Scanner
物件從標準輸入流讀取資料。 - 建立
TestParser
物件來處理輸入的資料。
2. 輸入資料的解析
TestParser
類中的parseInput
方法讀取輸入的字串,根據是否以#N
、#T
或#S
開頭,將資料分別追加到對應的StringBuilder
物件中。這裡沒有用String,是因為StringBuilder物件是可變的,可以動態地修改字串的內容,而不需要建立新的字串物件,方便我把各類語句存放到一起。
1 private StringBuilder str_N = new StringBuilder(); 2 private StringBuilder str_T = new StringBuilder(); 3 private StringBuilder str_S = new StringBuilder();
3. 題目資訊的解析
parseQuestions
方法將題目資訊從str_N
中解析出來,建立Question
物件,並儲存在陣列questions
中。
TestParser parser = new TestParser(); parser.parseInput(input); parser.parseQuestions(); // 呼叫解析題目資訊的方法
4. 試卷資訊的解析
parseExams
方法解析試卷資訊,建立Exam
物件,併為每個試卷新增題目和分值。題目從questions
陣列中獲取,併為每個題目設定分值。
parser.parseExams(); // 呼叫解析試卷資訊的方法
5. 答卷資訊的解析
parseAnswerSheets
方法解析答卷資訊,建立AnswerSheet
物件,併為每個答卷關聯對應的試卷和答案。
parser.parseAnswerSheets(); // 呼叫解析答卷資訊的方法
6. 試卷總分警示
- 遍歷
exams
陣列,如果任何試卷的總分不為100,輸出警示資訊。
for (Exam e : exams) { if (e.CalTotalscore() != 100) { System.out.printf("alert: full score of test paper%d is not 100 points\n", e.getNum()); } }
7. 判分和輸出結果
- 遍歷
answerSheets
陣列,對每個答卷呼叫CalAndJudge
方法判斷題目正確性並計算得分,並呼叫printf_result
方法輸出結果。 - 如果答卷對應的試卷不存在,輸出錯誤提示。
for (AnswerSheet a : answerSheets) { if (a != null) { a.CalAndJudge(); a.printf_result(); } if (a.getExam() == null) System.out.println("The test paper number does not exist"); }
第三次作業 答題判題程式-3
設計類圖如下:
在這個類圖中:
Main
類使用TestParser
類來解析輸入。TestParser
類建立Question
、Exam
、AnswerSheet
和Student
物件。Exam
類包含Question
物件。AnswerSheet
類使用Exam
類和Student
類來計算得分和輸出結果。Student
類包含AnswerSheet
物件,表示學生的答案資訊。
主要邏輯和詳細設計:
1. 初始化和輸入解析
- 使用
Scanner
物件從標準輸入流讀取資料。 - 建立
TestParser
物件來處理輸入的資料。
Scanner input = new Scanner(System.in); TestParser parser = new TestParser(); parser.parseInput(input);
-
解析學生資訊、題目資訊、試卷資訊、答卷資訊以及刪除題目資訊。
parser.parseStudent();
parser.parseQuestions();
parser.parseExams();
parser.parseAnswerSheets();
parser.parseDeleteQuestion();
- 獲取解析後的資料
Exam[] exams = parser.getExams(); AnswerSheet[] answerSheets = parser.getAnswerSheets(); Student[] students = parser.getStudents();
2. 輸入資料的解析
TestParser
類中的parseInput
方法讀取輸入的字串,根據是否以#N
、#T
、#S
、#X
或#D
開頭,將資料分別追加到對應的StringBuilder
物件中。
由於題目新增判斷輸入格式是否正確,所以這裡相較於第二次大作業新增了正規表示式去判斷每隔型別的輸入是否符合規範。
String questionPattern = "#N:(.*) #Q:(.*) #A:(.*)"; String examPattern = "#T:(\\d+)(?: \\d+-\\d+)*"; String studentPattern = "#X:(\\d+ .+)*"; String answerPattern = "#S:(\\d+) (\\d+)(?: #A:\\d+-.+)*"; String deletePattern = "#D:N-(\\d+)";
3. 題目資訊的解析
- 使用
split
方法按#N:
、#Q:
、#A:
分割輸入的字串,得到一個字串陣列parts_N
。 - 計算題目數量
num
,每個題目由三個部分構成:題目編號、內容、標準答案。 - 初始化
questions
陣列,長度為num
。 - 遍歷
parts_N
陣列,每三個元素構成一個題目。 - 對於每個題目,提取編號、內容和標準答案,建立
Question
物件,並將其新增到questions
陣列中。
4. 試卷資訊的解析
- 將輸入的試卷資料按行分割,得到
parts_T
陣列。 - 遍歷
parts_T
陣列,每行代表一張試卷。 - 對於每張試卷,提取試卷編號,並建立
Exam
物件。 - 遍歷試卷中的題目資訊,對於每個題目,提取題目編號和分值。
- 在
questions
陣列中查詢對應的題目,如果找到,則建立一個新的Question
物件,設定其分值,並將其新增到試卷中。 - 如果在
questions
陣列中沒有找到對應的題目,則建立一個內容為"none_null"
的Question
物件,並將其新增到試卷中。
for (Question q : questions) { if(q!=null) { if (q.getId() == Ques_id) { Question temp_q = new Question(q); exams[i].addQuestion(temp_q); temp_q.setScore(Ques_score); found = true; break; } } }
// 如果沒有找到題目,則新增錯誤題目 if (!found) { Question temp_q = new Question(); temp_q.setContent("none_null"); // 錯誤題號,題目內容設定為none_null temp_q.setScore(0); exams[i].addQuestion(temp_q); }
5. 學生資訊的解析
- 使用正規表示式分割輸入字串,獲取學生資訊。
- 計算學生數量並初始化學生陣列。
- 遍歷分割後的結果,提取學號和姓名。
- 為每個學生建立
Student
物件並儲存在陣列students中。
6. 答卷資訊的解析
- 將輸入的答卷資料按行分割,得到
parts_S
陣列。 - 遍歷
parts_S
陣列,每行代表一份答卷。 - 對於每份答卷,提取試卷號、學號,並建立
AnswerSheet
物件。 - 遍歷答卷中的答案,對於每個答案,提取答案內容,並將其新增到答卷中。
- 如果答卷中的題目編號與試卷中的題目順序不匹配,則在答卷中新增空答案。
- 遍歷學生陣列,查詢對應的學生,如果找到,則將答卷設定為該學生的答卷。
- 如果沒有找到對應的學生,則建立一個新的學生物件,並將答卷設定為其答卷。
for (int j = 2; j < S.length; j+=2) {//答卷上有答案的題目數量 if(correspondingExam!=null) { for (int k = 0; k < correspondingExam.getAllQuestions().size(); k++) { if (!S[j].trim().isEmpty()) { if (k + 1 == Integer.parseInt(S[j])) { if(j+1>=S.length) answerSheets[i].changeAnswer(" ", k+1 ); else answerSheets[i].changeAnswer(S[j+1].trim(), k+1 ); } else { if (!answerSheets[i].getAnswers().isEmpty()) { } else { answerSheets[i].changeAnswer(" ", k+1 ); //如果這個位置沒有被賦值,也就是不為空 } } } } } }
7. 刪除題目資訊的解析
- 分割輸入字串,獲取要刪除的題目編號。
- 遍歷所有試卷。
- 對於每個試卷,遍歷所有要刪除的題目編號。
- 在試卷中查詢並刪除指定編號的題目。
// 刪除題目 不是真的刪除,把被刪除的題目賦值為null public boolean deleteQuestionById(int id) { for (int i = 0; i < questions.size(); i++) { if (questions.get(i).getId() == id) { //questions.remove(i); questions.get(i).setContent("del_null"); questions.get(i).setScore(0); return true; // 返回成功 } } return false; // 如果沒有找到對應的題目,返回失敗 }
8. 輸出結果
-
檢查答卷是否關聯了有效的試卷: 對於每張答卷,首先檢查它是否關聯了一個有效的試卷(
a.getExam() != null
)。如果試卷不存在,則輸出提示資訊"The test paper number does not exist"
。 -
檢查試卷是否有題目: 如果試卷存在但試卷中沒有題目(
a.getExam().getAllQuestions().isEmpty()
),則輸出當前答卷關聯的學生資訊,但不包含答案和評分。 -
尋找答卷對應的學生並輸出結果: 如果試卷存在且有題目,則進一步檢查是否有學生與這張答卷關聯。這是透過遍歷
students
陣列來完成的。對於每個學生,檢查其答卷是否與當前遍歷的答卷a
相等。
如果找到了對應的學生,並且學生的名字不是預設的 "stu_null",則呼叫 a.CalAndJudge() 方法來計算分數,並呼叫 a.printf_result() 和 s.printStudents() 方法來輸出學生資訊和答卷結果;如果學生的名字是 "stu_null",表示學生資訊不完整,仍然計算分數和輸出答卷結果,會額外輸出 "not found" 提示。
三、踩坑心得
對原始碼的提交過程中出現的問題及心得進行總結,務必做到詳實,拿資料、原始碼及測試結果說話,切忌假大空
針對第二次大作業最後一題:
對於第二次大作業的最後一題,在拆解試卷資訊的parseExam()函式中,遍歷題庫去根據試卷順序新增題目和設定題目對應的分數時,不能直接像下面這樣:
for (Question q : questions) {
if (q.getId() == Ques_id) {
Question temp_q = new Question();
temp_q = q; // 這裡只是讓temp_q指向q所指向的物件,沒有建立新物件
exams[i].addQuestion(temp_q);
temp_q.setScore(Ques_score);
break;
}
}
正確的做法是建立一個新的 `Question` 物件,這樣每個試卷都有自己的題目物件,可以獨立地設定分值:
for (Question q : questions) {
if (q.getId() == Ques_id) {
Question temp_q = new Question(q); // 正確建立q的一個副本
exams[i].addQuestion(temp_q);
temp_q.setScore(Ques_score);
break;
}
}
針對第三次大作業最後一題:
1.正規表示式不夠準確,沒有考慮到 "#S:1 20201103 #A:1-5 #A:2-2 #B:3"這種情況下,是正確的格式,且"2 #B:3"是一個整體。
因此對個字串解析的正規表示式做出相應的修改:
修改了之後:
2.沒有考慮到試卷、問題、答卷是否為空,導致單資訊輸入非零返回。
對此新增相應判斷:
3.沒有考慮到答案為空字元的情況,空字元也算一個答案,空使用者答案和空標準答案匹配應為true
四、改進建議
對相應題目的編碼改進給出自己的見解,做到可持續改進
- 在動手寫程式碼之前,一定要先多讀幾遍題目內容,注意細節,尤其是針對各種情況要綜合考慮,以免出現不理清思路就開始敲,敲到後面發現自己不是漏了這一個細節,就是忘記考慮另一種情況,還要再反覆修改。
- 而且程式設計序的時候,一定要邊敲邊寫註釋,儘管當時你的思路是清晰的,可是等到下一次進階版大作業時,你想從原來的程式碼上去修改,就很費事了,因為,,已經逐漸看不懂自己敲得是什麼了。
- 還有就是實現程式碼功能的時候,對於複雜功能的實現,儘量將其拆分成多個更小的函式去實現,這樣既可以方便閱讀以及其他相關功能呼叫函式,還有助於程式碼的維護性,不然下一次大作業要對該功能進行修改,就比較費時費力了,
- 由於題目輸出的判斷情況較多,且輸出語句內容關乎到題目、答卷的等先後順序,這一塊比較容易混亂,寫著寫著就忘記了下一個判斷情況要寫什麼,所以要先理清這一塊的思路,最好先將其記錄、寫下來,這樣敲程式碼的時候方便直接對照著。
五、總結
對本階段三次題目集的綜合性總結,學到了什麼,哪些地方需要進一步學習及研究。
透過完成這三次答題判題程式的程式設計作業,我不僅加深了對Java程式設計的理解,而且對物件導向程式設計中的類的應用有了更加清晰的認識。這些練習讓我體會到了程式設計不僅僅是寫程式碼,更是一個涉及規劃、設計和最佳化的全面過程。
學到了在對題目進行程式設計之前,要先弄明白題目的目的,最好是把相關類圖給畫出來,方便後續寫程式碼思路更清晰。
然後,我體會到了程式碼重構的重要性。在實現複雜功能時,將大的程式碼塊分解成更小、更易於管理的函式。這樣做不僅使得程式碼更加模組化,也提高了程式碼的可讀性和可維護性。此外,透過消除重複程式碼,也能夠更快地進行後期修改,並能夠減少引入新bug的風險。
此外,我也認識到了測試的重要性,除去題目給的測試樣例,還應該去編寫其他不同情況下的測試樣例,透過編寫樣例去測試,能夠更好的找到程式的錯誤和不足之處。