圖的鄰接矩陣表示法可參考:http://www.lxweimin.com/p/9f27288f6749
測試圖如圖所示:
測試圖.png
普里姆(Prim)算法
思想:先選取一個頂點加入最小生成樹,再選取與該頂點相連的邊中的最小權值對應的頂點加入生成樹,將這兩個頂點作為一棵新的最小生成樹,繼續判斷與該樹相連的邊的最小權值對應的頂點,并將其加入最小生成樹,直到所有頂點均加入生成樹為止。
//最小生成樹--Prim算法
//每次都是從lowcost數組中選擇權值最小的邊加入生成樹
public void MiniSpanTree_Prim(MyGraph graph) {
int min, i, j, k;
//保存相關頂點下標
int[] adjvex = new int[graph.getNumOfVertex()];
//保存相關頂點間邊的權值
int[] lowcost = new int[graph.getNumOfVertex()];
/**
* lowcost值為0時表示此下標的頂點已經加入最小生成樹,也就是將v0將入生成樹
* 因為該程序默認從0號下標頂點開始建立最小生成樹,所以將lowcost初始化為0
* 但最小生成樹的建立并不一定需要從0號下標頂點開始
**/
lowcost[0] = 0;
//初始化第一個頂點的下標為0
adjvex[0] = 0;
for(i = 1; i < graph.getNumOfVertex(); i++) {
//將與v0頂點有邊的權值存入數組
lowcost[i] = edges[0][i];
//初始化都為v0的下標
adjvex[i] = 0;
}
for(i = 1; i < graph.getNumOfVertex(); i++) {
//初始化最小權值,一般設置為一個不可能的大數字,此處是65535
min = INF;
//j用來作為頂點下標的循環變量;k用來存儲最小權值頂點的下標
j = 1; k = 0;
while(j < graph.getNumOfVertex()) {
//如果權值不為0且權值小于min
if (lowcost[j] != 0 && lowcost[j] < min) {
//讓當前權值稱為最小值
min = lowcost[j];
//將當前最小值的下標存入k
k = j;
}
j++;
}
//輸出當前頂點邊中權值最小的邊
System.out.println("("+adjvex[k]+","+k+")");
//將當前頂點的權值設置為0,表示此頂點已經加入生成樹
lowcost[k] = 0;
for(j =1; j < graph.getNumOfVertex(); j++) {
//若下標為k的頂點各邊權值小于此前這些頂點未被加入生成樹權值
if (lowcost[j] != 0 && edges[k][j] < lowcost[j]) {
//將較小權值存入lowcost
lowcost[j] = edges[k][j];
//將下標為k的頂點存入adjvex
adjvex[j] = k;
}
}
}
}
測試程序
int n = 9;
String[] vertex = {"v0","v1","v2","v3","v4","v5","v6","v7","v8"};
MyGraph graph = new MyGraph(n);
for (String string : vertex) {
graph.insertVertex(string);
}
graph.insertEdge(0, 1, 10);
graph.insertEdge(0, 5, 11);
graph.insertEdge(1, 2, 18);
graph.insertEdge(1, 6, 16);
graph.insertEdge(1, 8, 12);
graph.insertEdge(2, 8, 8);
graph.insertEdge(2, 3, 22);
graph.insertEdge(3, 8, 21);
graph.insertEdge(3, 6, 24);
graph.insertEdge(3, 4, 20);
graph.insertEdge(3, 7, 16);
graph.insertEdge(4, 5, 26);
graph.insertEdge(4, 7, 7);
graph.insertEdge(5, 6, 17);
graph.insertEdge(6, 7, 19);
graph.MiniSpanTree_Prim(graph);
測試結果:
普里姆算法測試結果圖.png
克魯斯卡爾算法(Kruskal)
思想:將圖的存儲結構使用邊集數組的形式表示,并將邊集數組按權值從小到大排序,遍歷邊集數組,每次選取一條邊并判斷是否構成環路,不會構成環路則將其加入最小生成樹,最終只會包含n-1條邊(n為無向圖的頂點數)。
邊集數組的結構如圖所示:
邊集數組結構圖.png
邊集數組類
public class Edge implements Comparable<Edge> {
int begin;
int end;
int weight;
public int getBegin() {
return begin;
}
public void setBegin(int begin) {
this.begin = begin;
}
public int getEnd() {
return end;
}
public void setEnd(int end) {
this.end = end;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
@Override
public int compareTo(Edge o) {
// TODO Auto-generated method stub
//按權值升序排列
return this.weight - o.weight;
}
}
圖類
public class Graph {
private ArrayList<Object> vertexList; //存放頂點的數組
Edge[] edges; //邊集數組
int[][] arr; //鄰接矩陣
public Graph(int v, int e) {
//初始化結點數組
vertexList = new ArrayList<>(v);
//根據邊數初始化邊集數組
edges = new Edge[e];
//向邊集數組添加空對象
for(int i = 0; i < e; i++){
Edge edge = new Edge();
edges[i] = edge;
}
//初始化鄰接矩陣
arr = new int[v][v];
for (int i = 0; i < v; i++) {
for (int j = 0; j < v; j++) {
if (i == j)
arr[i][j] = 0;
else {
arr[j][i] = 65535;
}
}
}
}
//插入結點
public void insertVertex(Object vertex) {
vertexList.add(vertexList.size(),vertex);
}
//插入無向邊以及設置權值
public void insertEdge(int n1, int n2, int weight) {
arr[n1][n2] = weight;
//該圖為無向圖,所以矩陣關于對角線對稱
arr[n2][n1] = weight;
}
//獲取頂點個數
public int getNumOfVertex() {
return vertexList.size();
}
//獲取頂點n的值
public Object getValueByIndex(int n) {
return vertexList.get(n);
}
//鄰接矩陣轉換為邊集數組
public void MatrixToEdgesArray() {
int k = 0;
//因為該圖為無向圖,所以該矩陣關于從左上到右下的對角線對稱
//因此此處遍歷矩陣只需遍歷右上部分即可
for (int i = 0; i < getNumOfVertex(); i++) {
for (int j = i ; j < getNumOfVertex(); j++) {
if (arr[i][j] < 65535 && arr[i][j] != 0) {
edges[k].begin = i; // 編號較小的結點為首
edges[k].end = j; // 編號較大的結點為尾
edges[k].weight = arr[i][j];
k++;
}
}
}
//將edges邊集數組按權值從小到大排序
Arrays.sort(edges);
}
//最小生成樹--克魯斯卡爾算法
public void MiniSpanTree_Kruskal(Graph graph) {
int i, n, m;
//將鄰接矩陣轉換為邊集數組
MatrixToEdgesArray();
int[] parent = new int[edges.length];
//初始化parent數組,用于判斷是否產生了環路
for(i = 0; i < edges.length; i++) {
parent[i] = 0;
}
for(i = 0; i < edges.length; i++) {
//按權值從小到大拿到每一條邊
Edge edge = edges[i];
n = Find(parent,edge.begin);
m = Find(parent,edge.end);
//n==m時表示構成了環路,不能納入最小生成樹中
if (n != m) {
System.out.println("(" + edge.begin + "," + edge.end + ")-->" + edge.weight);
parent[n] = m;
}
}
}
public int Find(int[] parent, int f) {
/**當i=7時,parent數組為{1,5,8,7,7,8,0,0,6}
* 對應的下標為{0,1,2,3,4,5,6,7,8}
*parent[0] = 1表示頂點0,1已經加入生成樹中
*所以此時頂點0,1,2,5,8,6在一個邊集合中;頂點3,4,7在一個邊集合中
**/
while(parent[f] > 0) {
f = parent[f];
}
return f;
}
}
測試程序:
int n = 9, e = 15;
String[] vertex = {"v0","v1","v2","v3","v4","v5","v6","v7","v8"};
Graph graph = new Graph(n,e);
for (String string : vertex) {
graph.insertVertex(string);
}
graph.insertEdge(0, 1, 10);
graph.insertEdge(0, 5, 11);
graph.insertEdge(1, 2, 18);
graph.insertEdge(1, 6, 16);
graph.insertEdge(1, 8, 12);
graph.insertEdge(2, 8, 8);
graph.insertEdge(2, 3, 22);
graph.insertEdge(3, 8, 21);
graph.insertEdge(3, 6, 24);
graph.insertEdge(3, 4, 20);
graph.insertEdge(3, 7, 16);
graph.insertEdge(4, 5, 26);
graph.insertEdge(4, 7, 7);
graph.insertEdge(5, 6, 17);
graph.insertEdge(6, 7, 19);
graph.MiniSpanTree_Kruskal(graph);
測試結果:
克魯斯卡爾算法測試結果圖.png
最小生成樹為:
最小生成樹.png
總結
普里姆算法針對頂點展開,通過不斷尋找與已構建的生成樹的最小邊來不斷構建新的生成樹。普里姆算法對于稠密圖,也就是邊數非常多的情況會更好一些,因為其是通過頂點來展開的。算法時間損耗主要來源于嵌套的for循環,所以時間復雜度為O(n^2)。
克魯斯卡爾算法針對邊展開,通過對邊集數組的遍歷來構建最小生成樹,但是過程中必須避免構成環路。克魯斯卡爾算法對于稀疏圖,也就是邊數較少的情況效率會很高。此算法的Find函數由邊數e決定,時間復雜度為O(loge),再加上外層for循環的e次,所以時間復雜度為O(eloge)。