2020春計算機學院《軟件構造》課程Lab1實驗報告
1 實驗目標概述
本次實驗通過求解三個問題,訓練基本 Java 編程技能,能夠利用 Java OO 開發基本的功能模塊,能夠閱讀理解已有代碼框架並根據功能需求補全代碼,能夠爲所開發的代碼編寫基本的測試程序並完成測試,初步保證所開發代碼的正確性。另一方面,利用 Git 作爲代碼配置管理的工具,學會 Git 的基本使用方法。
基本的 Java OO 編程
基於 Eclipse IDE 進行 Java 編程
基於 JUnit 的測試
基於 Git 的代碼配置管理
2 實驗環境配置
2.1 Java & Eclipse & Maven 使用配置方法
2.2 JUnit
3 實驗過程
3.1 Magic Squares
- 幻方是一個有n*n個不同數字、且每行、每列和斜線上都有相同的和的方形結構。要求寫出程序判斷輸入一個矩陣是否是幻方,並且構造幻方。
- Main函數有兩個部分,分別是:讀取5個矩陣並判斷、生成一個矩陣判斷後輸出到文件中。在讀取時,用for循環分別將字符1到5拼入地址中,輸出同理。其中在生成幻方之前,判斷n的合法性。
static final int N = 200;
public static int[][] square = new int[N][N];
public static boolean[] vis = new boolean[N * N + 1];
public static void main(String[] args) throws IOException {
for (char i = '1'; i <= '5'; i++) {
System.out.println(i + " " + String.valueOf(isLegalMagicSquare("src/P1/txt/" + i + ".txt")));
}
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
while (n <= 0 || n % 2 == 0) {
System.out.println("Input Wrong");
n = sc.nextInt();
}
generateMagicSquare(n);
System.out.println("6" + " " + String.valueOf(isLegalMagicSquare("src/P1/txt/" + "6" + ".txt")));
return;
}
3.1.1 isLegalMagicSquare()
該函數要實現判斷一個矩陣是否爲幻方。
- 讀入文件
a) 創建FileReader、BufferReader、StringBuilder對象
b) 初始化line - 逐行將字符串轉換爲整型矩陣存儲
a) 將非空readline的字符串分割
b) 去除頭尾空格後轉換爲數字存儲到二維數組中
c) 存儲時判斷該數字是否出現過
i. 出現過則報錯
ii. 否則用Boolean表標記
d) 判斷行列長度是否相等 - 計算兩條斜線的和並比較
a) 分別得出主對角線和次對角線的和
b) 比較
i. 若相等則記錄,作爲基準值
ii. 若不相等則報錯 - 計算每條縱線和橫線和和並比較
a) 分別計算第i條橫線和縱線的和
b) 與基準值比較
i. 不相等則報錯 - 確定是否爲幻方
public static boolean isLegalMagicSquare(String fileName) throws IOException {
File file = new File(fileName);
FileReader reader = new FileReader(file);
BufferedReader bReader = new BufferedReader(reader);
StringBuilder sb = new StringBuilder();
String line = "";
int n = 0, m = 0;
Arrays.fill(vis, false);
while ((line = bReader.readLine()) != null) {
String[] l = line.split("\t");
m = l.length;
for (int i = 0; i < m; i++) {
square[n][i] = Integer.valueOf(l[i].trim());
if (square[n][i] <= 0 || vis[square[n][i]])
return false;
else
vis[square[n][i]] = true;
}
n++;
}
bReader.close();
if (n != m)
return false;
int s1 = 0, s2 = 0, s = 0;
for (int i = 0; i < n; i++) {
s1 += square[i][i];
s2 += square[n - i - 1][i];
}
if (s1 == s2)
s = s1;
else
return false;
for (int i = 0; i < n; i++) {
s1 = s2 = 0;
for (int j = 0; j < n; j++) {
s1 += square[i][j];
s2 += square[j][i];
}
if (s1 != s || s2 != s)
return false;
}
return true;
}
3.1.2 generateMagicSquare()
該函數要實現生成一個邊長爲奇數的幻方。
- 初始化
i. 生成空矩陣
ii. Row=0,Col=n/2 - 循環n*n次填充矩陣
i. 將矩陣的[row, col]位置填充爲i
ii. 在保證座標在矩陣範圍內的情況下使Row–,Col++ - 打開文件,打印結果
public static boolean generateMagicSquare(int n) throws IOException {
int magic[][] = new int[n][n];
int row = 0, col = n / 2, i, j, square = n * n;
for (i = 1; i <= square; i++) {
magic[row][col] = i;
if (i % n == 0)
row++;
else {
if (row == 0)
row = n - 1;
else
row--;
if (col == (n - 1))
col = 0;
else
col++;
}
}
File file = new File("src/P1/txt/6.txt");
PrintWriter output = new PrintWriter(file);
for (i = 0; i < n; i++) {
for (j = 0; j < n; j++)
output.print(magic[i][j] + "\t");
output.println();
}
output.close();
return true;
}
3.2 Turtle Graphics
- 該任務需要我們clone已有的程序後,利用turtle按照要求畫圖,其中需要利用幾何知識設計一些函數簡化編程,最後可以發揮想象力進行Personal Art。首先分析turtle的package組成,瞭解類成員。
3.2.1 Problem 1: Clone and import
- 打開目標存儲文件夾 右鍵點擊Git Bash
- 輸入git clone https://github.com/ComputerScienceHIT/Lab1-1183710109.git
3.2.2 Problem 3: Turtle graphics and drawSquare
- 該函數需要實現:已知邊長,畫出邊長爲指定數值的正方形。參數是海龜對象turtle和編程sidelength。
- 首先將海龜畫筆設置爲黑色。然後執行4次的前進sidelength長度、轉完90度,即可完成一個邊長爲sidelength的正方形。下圖是邊長爲200的正方形:
public static void drawSquare(Turtle turtle, int sideLength) {
turtle.color(PenColor.BLACK);
for (int i = 0; i < 4; i++) {
turtle.forward(sideLength);
turtle.turn(90);
}
}
3.2.3 Problem 5: Drawing polygons
- 該問題首先希望已知正多邊形邊數的情況下計算正多邊形的內角度。根據幾何知識可以推導得公式:
- 使用該公式,實現calculateRegularPolygonAngle,通過運行TurtleSoupTest中的Junit測試得:
public static double calculateRegularPolygonAngle(int sides) {
return (double) 180.0 - (double) 360.0 / sides;
}
- 該問題還希望已知正多變型得邊數和邊長畫出一個正多邊形。參照畫正方形的方法,可以先前進sidelength,再使海龜旋轉一個角度,執行“邊數”次。其中,這個角度是正多邊形內角的補角,利用calculateRegularPolygonAngle的功能計算出多邊形內角,再用180°減去這個值即可。邊長爲100的正六邊形效果如下:
public static void drawRegularPolygon(Turtle turtle, int sides, int sideLength) {
turtle.color(PenColor.BLACK);
for (int i = 0; i < sides; i++) {
turtle.forward(sideLength);
turtle.turn((double) 180.0 - calculateRegularPolygonAngle(sides));
}
}
3.2.4 Problem 6: Calculating Bearings
該問題首先希望解決,已知起點和當前朝向角度,想知道到終點需要轉動的角度。例如,如果海龜在(0,1)朝向 30 度,並且必須到達(0,0)它必須再轉動 150 度。
- 首先使用Math.atan2函數計算兩點之間的邊在座標系的角度,減去當前朝向的角度;
- 然後取相反數(海龜旋轉的方向是順時針,座標軸角度的旋轉角度的逆時針);
- 再減去90°(海龜的0°線是向上,座標軸的0°線是向右,向右到向上要逆時針旋轉90°);
- 最後調整爲0-360°之間(可能大於360°或小於0°)。
public static double calculateBearingToPoint(double currentBearing, int currentX, int currentY, int targetX,
int targetY) {
double angle = Math.atan2(targetY - currentY, targetX - currentX) * 180.0 / Math.PI;
if (angle < 0)
angle += 360.0;
double bearing = (360 - angle + 90 >= 360 ? 90 - angle : 360 - angle + 90) - currentBearing;
return bearing < 0 ? 360.0 + bearing : bearing;
}
- 基於上一個問題,此時有若干個點,想知道從第一個點開始到第二個點,再從第二個點到第三個點……以此類推每次轉向的角度。
- 將“起點”選爲第一個點(座標爲(xCoords.get(0),yCoords.get(0)));
- 循環n-1次(n爲點的個數)
- 每次將第i+1號點設置爲“終點”,通過上一個函數計算旋轉角度並存儲到List中;
- 將下一次的“起點”用當前“終點”更新,繼續循環;
- 退出循環後返回List。
public static List<Double> calculateBearings(List<Integer> xCoords, List<Integer> yCoords) {
double currentBearing = 0.0;
int currentX = xCoords.get(0), currentY = yCoords.get(0), targetX, targetY;
int length = xCoords.size();
List<Double> ans = new ArrayList<>();
for (int i = 1; i < length; i++) {
targetX = xCoords.get(i);
targetY = yCoords.get(i);
ans.add(calculateBearingToPoint(currentBearing, currentX, currentY, targetX, targetY));
currentBearing = ans.get(i - 1);
currentX = targetX;
currentY = targetY;
}
return ans;
}
3.2.5 Problem 7: Convex Hulls
3.2.5.1 凸包問題
3.2.5.2 算法描述
3.2.5.3 JUnit測試結果
3.2.6 Problem 8: Personal art
3.2.7 Submitting
3.3 Social Network
- 該任務要求設計一張社交網絡圖,基於連接人與人,並且能計算任意兩人之間的聯繫情況。網絡圖基於兩個類,分別是FriendshipGraph類和Person類。
3.3.1 設計/實現FriendshipGraph類
- 該類的實際意義是一張社交網絡圖,包括了代表每個Person的點、代表每兩個Person之間聯繫的邊、以及建立點和聯繫和計算距離的方法。
3.3.1.1 鄰接表存儲結構
- 在存儲社交網絡時,我使用了鄰接表。所有的Node被連在一起,方便查找,並補充了一個head變量用來標記首個Node。假定一個社交網絡爲
- 則該圖轉換爲鄰接表的示意圖爲:
3.3.1.2 Class Node
- Node類要實現的是將一個Person轉換爲鄰接表裏的點,所以一個Node有鄰接表中點的重要成員變量:下一個Node爲next,對應的Person對象person,直接連接的邊lastEdge,以及實現鄰接表的相關方法。此外,爲了能實現方法getDistance,我另外增設了vis和dis兩個變量用來記錄是否訪問過以及與當前起點的最近距離。
- Class Node.Edge
該類是鄰接表中的邊,每個Edge對象存儲了鄰接表中的下一條邊,以及對應的邊的兩個Person所對應的Node。 - Method Node.LoadData
該方法將Person對象導入到Node中進行存儲,需要的時候可以直接調用。 - Method Node.addNode
該方法將相應的Node加入到Node的鏈表中(即鄰接表圖中的縱向鏈表)。 - Method Node.addNodeEdge
該方法將新的邊加入到對應的Node中,更新每個Node後的Edge鏈表。
/**
* @Class Node : A Node is linked with a particular Person based on graph
* theory. TODO Build nodes and Linking persons
* @param next the person joins in the graph after him/her
* @param person class Person matched with
* @param vis visited mark for BFS
* @param dis lowest distance from a chose person
* @param lastEdge first edge in First-Search
* @method LoadData record which person THIS belongs to
* @method addNode add a new Node for a person
* @method addNodeEdge add a edge between 2 Nodes
*/
public class Node {
private Node next = null;
Person person;
private boolean vis;
private int dis = Integer.MAX_VALUE;
private Edge lastEdge = null;
public void LoadData(Person person) {
this.person = person;
person.node = this;
}
public void addNode(Node nextVertex) {
nextVertex.next = this.next;
this.next = nextVertex;
}
/**
* @Class Edge A edge between 2 Nodes
* @param origin one Node
* @param terminal another Node
* @param nextEdge the next edge which has the same ORIGIN with THIS
*/
public class Edge {
public Node origin = null, terminal = null;
public Edge nextEdge = null;
}
public void addNodeEdge(Node toVertex) {
Edge newEdge = new Edge();
newEdge.origin = this;
newEdge.terminal = toVertex;
newEdge.nextEdge = this.lastEdge;
this.lastEdge = newEdge;
}
}
3.3.1.3 Method addVertex()
- 該方法目標是在社交網絡圖中增加一個新的節點,參數是要加入的Person類。首先,方法要對Person的名字進行判重:用哈希集合HashSet記錄下已加入的所有Person的名字,每當新加入一個Person則進行判斷是否在集合中;然後則新建一個Node類,使每一個Person與一個Node對應起來。
/**
* @method addVertex add new vertex if the person's name is unduplicated
* @param newPerson adding person
* @param head head of Linked List of persons
* @param NameSet set of persons' names, used for removing duplication
*/
private Node head = null;
private HashSet<String> NameSet = new HashSet<>();
public void addVertex(Person newPerson) {
if (NameSet.contains(newPerson.Name)) {
System.out.println("Person " + newPerson.Name + " already existed.");
System.exit(0);
}
NameSet.add(newPerson.Name);
Node NewVertex = new Node();
newPerson.node = NewVertex;
NewVertex.LoadData(newPerson);
if (head == null)
head = NewVertex;
else
head.addNode(NewVertex);
return;
}
3.3.1.4 Method addEdge()
- 該方法目標是將兩個Person之間進行聯繫,在鄰接表中,用有向邊來代表“有社交關係”,由於題目設定是社交默認爲雙向,則需要在函數中兩次調用Node中的addNodeEdge方法加兩個方向的邊。考慮到可擴展性和可複用性,程序考慮到了“單向社交的情況”,僅需將雙向加邊中的“B->A”刪除即可。
/**
* @method addEdge add edges of double directions
* @param a,b 2 persons being linking with an edge
* @param A,B 2 nodes of the 2 persons
*/
public void addEdge(Person a, Person b) {
if (a == b) {
System.out.println("They are the same one.");
System.exit(0);
}
Node A = a.node, B = b.node;
A.addNodeEdge(B);
B.addNodeEdge(A);
return;
}
3.3.1.5 Method getDistance()
- 該方法要計算任意兩個Person之間的“距離”,若沒有任何社交關係則輸出“-1”。兩個Person之間計算使用BFS,默認邊權爲1,則在搜索到邊時加1即可,搜索到目標點退出;特殊情況根據要求輸出0或-1。
/**
* @method getDistance
* @param sta path starting person
* @param end path ending person
* @return distance between 2 persons or -1 when unlinked
*/
public int getDistance(Person sta, Person end) {
if (sta == end)
return 0;
Queue<Person> qu = new LinkedList<Person>();
for (Node p = head; p != null; p = p.next) {
p.vis = false;
p.dis = 0;
}
sta.node.vis = true;
for (qu.offer(sta); !qu.isEmpty();) {
Person p = qu.poll();
for (Node.Edge e = p.node.lastEdge; e != null; e = e.nextEdge) {
if (!e.terminal.vis) {
qu.offer(e.terminal.person);
e.terminal.vis = true;
e.terminal.dis = p.node.dis + 1;
if (e.terminal.person == end)
return end.node.dis;
}
}
}
return -1;
}
3.3.2 設計/實現Person類
- 該類的目標是將每一個人對應到一個Person對象,並存儲名字的信息。此外,在我的設計中,爲了方便起見,我將每個Person對象在FriendshipGraph中對應的Node存儲在對應Person的成員變量中。
public class Person {
public String Name;
public FriendshipGraph.Node node = null;
public Person(String PersonName) {
Name = PersonName;
}
}
3.3.3 設計/實現客戶端代碼main()
3.3.3.1 重複名字錯誤測試
/**
* TODO to test program exit when graph has the same names.
*/
@Test
void ExceptionProcess() {
FriendshipGraph graph = new FriendshipGraph();
Person a = new Person("a");
graph.addVertex(a);
Person b = new Person("b");
graph.addVertex(b);
// Person c = new Person("a");
// graph.addVertex(c);
}
3.3.3.2 簡單圖測試
/**
* Basic Network Test
*/
@Test
void GraphTest1() {
FriendshipGraph graph = new FriendshipGraph();
Person rachel = new Person("Rachel");
Person ross = new Person("Ross");
Person ben = new Person("Ben");
Person kramer = new Person("Kramer");
graph.addVertex(rachel);
graph.addVertex(ross);
graph.addVertex(ben);
graph.addVertex(kramer);
graph.addEdge(rachel, ross);
graph.addEdge(ross, rachel);
graph.addEdge(ross, ben);
graph.addEdge(ben, ross);
/*
* System.out.println(graph.getDistance(rachel, ross));// 1
* System.out.println(graph.getDistance(rachel, ben));// 2
* System.out.println(graph.getDistance(rachel, rachel));// 0
* System.out.println(graph.getDistance(rachel, kramer));// -1
*/
assertEquals(1, graph.getDistance(rachel, ross));
assertEquals(2, graph.getDistance(rachel, ben));
assertEquals(0, graph.getDistance(rachel, rachel));
assertEquals(-1, graph.getDistance(rachel, kramer));
}
3.3.3.3 複雜圖測試
/**
* Further Test
*/
@Test
void GrpahTest2() {
FriendshipGraph graph = new FriendshipGraph();
Person a = new Person("A");
Person b = new Person("B");
Person c = new Person("C");
Person d = new Person("D");
Person e = new Person("E");
Person f = new Person("F");
Person g = new Person("G");
Person h = new Person("H");
Person i = new Person("I");
Person j = new Person("J");
graph.addVertex(a);
graph.addVertex(b);
graph.addVertex(c);
graph.addVertex(d);
graph.addVertex(e);
graph.addVertex(f);
graph.addVertex(g);
graph.addVertex(h);
graph.addVertex(i);
graph.addVertex(j);
graph.addEdge(a, b);
graph.addEdge(a, d);
graph.addEdge(b, d);
graph.addEdge(c, d);
graph.addEdge(d, e);
graph.addEdge(c, f);
graph.addEdge(e, g);
graph.addEdge(f, g);
graph.addEdge(h, i);
graph.addEdge(i, j);
assertEquals(2, graph.getDistance(a, e));
assertEquals(1, graph.getDistance(a, d));
assertEquals(3, graph.getDistance(a, g));
assertEquals(3, graph.getDistance(b, f));
assertEquals(2, graph.getDistance(d, f));
assertEquals(2, graph.getDistance(h, j));
assertEquals(0, graph.getDistance(i, i));
assertEquals(-1, graph.getDistance(d, j));
assertEquals(-1, graph.getDistance(c, i));
assertEquals(-1, graph.getDistance(f, h));
}
3.3.4 設計/實現測試用例
3.3.4.1 重複名字錯誤測試
- 測試當有重複名字“a”時,程序是否能終止
Person a = new Person("a");
graph.addVertex(a);
Person b = new Person("b");
graph.addVertex(b);
Person c = new Person("a");
graph.addVertex(c);
3.3.4.2 簡單圖測試
- 根據題目中的社交網絡圖:
- 分別測試:
- Rachel和Ross距離是1,Rachel和Ben距離是2
- Rachel和Rachel距離是0
- Rachel和Kramer距離是-1
3.3.4.3 複雜圖測試
- 設計10個點、10條邊的社交網絡圖:
- 分別測試:
- AE距離2,AD距離1,AG距離3,BF距離3,DF距離2,HJ距離2
- II距離0
- DJ距離-1,CI距離-1,FH距離-1
3.3.4.4 Junit測試結果
4 實驗進度記錄
- 略
5 實驗過程中遇到的困難與解決途徑
6 實驗過程中收穫的經驗、教訓、感想
- 略