OO第三單元回顧總結
前言
本單元圍繞JML進行規格化設計,契約式程式設計的思想基本上貫穿了整個單元。
本單元最大的新體驗在於:作為大工程中某一部分程式碼的實現者,履行應盡的責任,而非像前幾個單元一樣自行設計完整架構並完成所有程式碼。更直觀的感受在於:由於課程組提供了細化到每一個類和類方法的JML描述,完成作業更像是做一道道小題,頗有一種“闖關”的快感,寫起程式碼來體驗極佳!
儘管如此,本單元依然存在一些難點,也存在許多值得探討的課題,接下來將詳細描述。
一、架構設計
本單元主要需要設計Network
中的Gruop
、Person
例項的生成、關係建立和查詢等方法。其中涉及一些圖論知識。
1. 第一次作業
第一次作業的難點在於完成query_circle
和query_block_sum
兩種查詢。在實現中,本人使用了並查集演算法。
具體而言:結點的連通關係是一種等價關係,具有自反性、對稱性和傳遞性,圖中的所有結點可以被分成多個連通分量(參考離散數學知識)。而本單元Person
例項間的連通關係是同樣的道理,因此可以根據是否連通,將Person
劃入到多個集合中,互相連通的Person
在程式碼實現上,集合的概念通過並查集演算法實現,位於同一個集合的Person
例項形成一個樹狀結構,則在查詢時,可:
- 通過判斷兩個
Person
所在樹狀結構的根結點是否相同,判斷兩個Person
是否連通,完成query_circle
查詢 - 通過檢視有多少個不同的根結點,完成
query_block_sum
查詢
2. 第二次作業
第二次作業的難點在於完成query_least_connection
查詢,需要設計最小生成樹的演算法。在實現中,本人使用了Kruskal演算法。
具體而言:Kruskal演算法不斷從未選中的邊集中選擇權重最小的邊,嘗試將此邊加入已選中的邊集,並判斷引入此邊是否會導致環
假如當前結點連線如左圖所示,且Kruskal將繼續嘗試將Edge1和Edge2加入邊集:在判斷Edge1時:由於結點2和3已經位於同一個集合,因此無法繼續加入;而在判斷Edge2時:結點5和7位於兩個不同的集合,因此可加入此邊。
3. 第三次作業
第三次作業的難點在於完成send_indirect_message
查詢,需要完成最短路徑演算法。在實現中,本人使用了堆優化的Dijkstra演算法。
具體而言:Dijkstra演算法從當前未到達的結點集合中,選取最近的結點標記為到達,並用此節點更新其他未到達結點的距離。由於需要不斷更新結點的當前距離,並涉及選取最近的結點,因此類似於優先佇列問題,可以使用小頂堆進行優化。在本人的具體實現中,使用了Java自帶的PriorityQueue
容器完成。
二、測試資料準備
本單元需要利用JML規格來準備測試資料。總結而言,滿足各個類方法前置條件的測試資料均應該覆蓋。而本單元真正具有前置條件的類方法非常少,大部分都通過丟擲異常解決,因此,測試資料就幾乎涵蓋了所有可能的情況。
具體來說,本人在構造測試資料時,主要分別構造正常資料和異常資料,並分別對每個異常都構造資料。以addToGruop
方法為例:
addToGroup
方法設計到三種異常。因此,在構造測試資料時,需要包含:
- 正常的測試資料:存在
id
為id1
的Person
,存在id
為id2
的Group
,且Person
目前不在Group
中 - 涉及
GroupIdNotFoundException
的測試資料:不存在id
為id2
的Group
- 涉及
PersonIdNotFoundException
的測試資料:不存在id
為id1
的Person
- 涉及
EqualPersonIdException
的測試資料:id
為id1
的Person
已經位於id
為id2
的Group
中
三、效能分析
本單元嚴格限制了程式碼執行時間,以下總結了可能出現效能問題的指令,與本人的解決方案。
1. query_circle與query_block_sum
除了上文中提到的,這兩條指令使用並查集完成外。在實現過程中,還需要進行路徑壓縮。具體而言,路徑壓縮通過重構並查集的樹狀結構,減少查詢根結點時間。以下圖為例:
在查詢到結點3的根結點為結點1之後,使結點3直接指向結點1,由此可以減少下次查詢結點3根結點時花費的時間。
2. query_group_value_sum
如果在每次收到qgvs
指令後,都通過二重迴圈,遍歷Group中兩兩結點的關係,則時間複雜度為O(n^2),無法通過本單元測試。解決方案是,在Group中維護一個變數valueSum
儲存此查詢的答案。valueSum
需要在遇到以下指令時檢查是否需要更新:
-
add_relation:需要檢查兩個
Person
是否都位於同一個Group
,如果是,則需要更新valueSum
-
add_to_group:需要檢查加入的
Person
與目前位於Group
中的每一個Person
是否有關係,如果是,則需要更新valueSum
-
del_from_group:需要檢查刪除的
Person
與目前位於Group
中的其他每一個Person
是否有關係,如果是,則需要更新ValueSum
3. query_least_connection
如上文所述,利用依賴並查集的Kruskal演算法,可以滿足時間限制。
4. send_indirect_message
如上文所述,利用堆優化的Dijkstra演算法,可以滿足時間限制。
四、Network拓展
本人思路如下:構建Advertiser
、Producer
、Customer
三個介面,這三個介面均繼承Person
介面。並構建Product
類,封裝產品資訊。
Advertiser
介面見下。Advertise
維護productInfo
陣列,通過addAdvertise()
方法接受來自Producer
的產品資訊,通過getAdvertise()
方法提供產品資訊。
Producer
介面見下。Producer
擁有一個Product
例項,封裝自身產品資訊,同時Producer
擁有一個Message
變數,存放買家資訊。Producer
通過useAdvertise()
方法向Advertiser
提供自身產品資訊,通過receiveBuyInfo()
方法獲取買家資訊。
Customer
介面見下。Customer
通過buy()
方法向Advertiser
表明購買意願。由於buy()
方法設計消費者的具體偏好,因此此處並未給出具體的JML規格。
五、學習體會
總結而言,本單元學習了契約式程式設計的重要思想,並通過三次單元作業,掌握了讀寫JML規格語言的能力,能根據JML規格語言實現程式碼。就筆者而言,這是頭一次涉及到多人合作完成程式碼的方法和思想,有非常獨特的體驗。
在完成單元作業方面,本單元確實難度較低,學習過程非常輕鬆愉快。同時,本單元間接幫助筆者回憶了離散數學和資料結構的一些相關知識。也只有在這種情況下,筆者才能意識到自己對於過去學過的知識遺忘之快,可見過去學習並不那麼紮實。
最後,希望能順利完成第四單元,繼續加油!