最小生成樹之Kruskal算法

[b]上接面向對象方式實現最小生成樹算法[/b]
[url]http://zhuyufufu.iteye.com/blog/1989304[/url]

這篇文章實現[b]最小生成樹[/b]的[b]Kruskal算法[/b]

[b]Kruskal算法:[/b]
Kruskal算法思想不一樣於Prim算法,Kruskal算法是一種按照連通網中邊的權值的遞增順序構造最小生成樹的算法。

[b]Kruskal算法的基本步驟 :[/b]
假設G=(V,E)是一個具備n個頂點的連通網,T=(U,TE)是G的最小生成樹。
令集合U的初值爲U=V,即包含有G中所有頂點,集合TE的初值爲TE={}。
而後,將圖G中的邊按權值從小到大的順序依次選取,若選取的邊使生成樹T不造成迴路,則把它併入TE中,保留做爲T的一條邊;
若選取的邊使生成樹T造成迴路,則將其捨棄,如此進行下去,直到TE中包含有n-1條邊爲止,此時的T即爲最小生成樹。


另外對上一篇文章中的程序進行了重構優化。

展現代碼以下:

點是否在邊中調整到邊類

package com.zas.test.tree;

/**
* 邊的定義
* @author zas
*/
public class Edge {
//起點
protected Point startPoint;
//終點
protected Point endPoint;

public Edge() {
super();
}

public Edge(Point startPoint, Point endPoint) {
super();
this.startPoint = startPoint;
this.endPoint = endPoint;
}

public Point getStartPoint() {
return startPoint;
}

public void setStartPoint(Point startPoint) {
this.startPoint = startPoint;
}

public Point getEndPoint() {
return endPoint;
}

public void setEndPoint(Point endPoint) {
this.endPoint = endPoint;
}

/* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
* 只有起止點相同的邊纔是同一條邊
*/
@Override
public boolean equals(Object obj) {
if(obj instanceof Edge){
Edge outEdge = (Edge)obj;
if(outEdge.startPoint.equals(this.startPoint)){
if(outEdge.endPoint.equals(this.endPoint)){
return true;
}
}
}
return false;
}

@Override
public String toString() {
return "Edge [startPoint=" + startPoint + ", endPoint=" + endPoint
+ "]";
}

/**
* 點是否在邊中
* @param p
* @param edge
* @return
*/
public boolean isPointInEdge(Point p) {
if(p.equals(this.getStartPoint()) || p.equals(this.getEndPoint())){
return true;
}
return false;
}
/**
* @param args
*/
public static void main(String[] args) {

}

}



[b]判斷圖是否含有環,圖頂點度的計算調整到圖類[/b]

package com.zas.test.tree;

import java.util.ArrayList;
import java.util.List;

/**
* 圖的定義
* @author zas
*/
public class Graph {
//圖的點列表
List<Point> pointList;
//圖的邊列表
List<EdgeWithWeight> edgeList;

public Graph() {
super();
}

public Graph(List<Point> pointList, List<EdgeWithWeight> edgeList) {
super();
this.pointList = pointList;
this.edgeList = edgeList;
}

/**
* 添加一個點到圖中
* @param p
*/
public void addPoint(Point p) {
if(pointList == null){
pointList = new ArrayList<Point>();
}
pointList.add(p);
}

/**
* 從圖中刪除一個點
* @param p
*/
public void removePoint(Point p){
for (Point point : pointList) {
if(p.equals(point)){
pointList.remove(point);
break;
}
}
}

/**
* 添加一條邊到圖中
* @param p
*/
public void addEdge(EdgeWithWeight e) {
if(edgeList == null){
edgeList = new ArrayList<EdgeWithWeight>();
}
edgeList.add(e);
}

/**
* 從圖中刪除一條邊
* @param p
*/
public void removeEdge(EdgeWithWeight e) {
for (EdgeWithWeight edge : edgeList) {
if(e.equals(edge)){
edgeList.remove(edge);
break;
}
}
}

/**
* 點是否存在於圖中
* @param p
* @return
*/
public boolean isPointInGraph(Point p) {
if(null == pointList || pointList.size() < 1){
return false;
}
for (Point point : pointList) {
if(p.equals(point)){
return true;
}
}
return false;
}

/**
* 點是否存在於圖中
* @param p
* @return
*/
public boolean isEdgeInGraph(EdgeWithWeight e) {
if(null == edgeList || edgeList.size() < 1){
return false;
}
for (EdgeWithWeight edge : edgeList) {
if(e.equals(edge)){
return true;
}
}
return false;
}

public List<Point> getPointList() {
return pointList;
}

public void setPointList(List<Point> pointList) {
this.pointList = pointList;
}

public List<EdgeWithWeight> getEdgeList() {
return edgeList;
}

public void setEdgeList(List<EdgeWithWeight> edgeList) {
this.edgeList = edgeList;
}

@Override
public String toString() {
return "Graph [pointList=" + pointList + ", edgeList=" + edgeList + "]";
}

@Override
public Graph clone() {
Graph g = new Graph();
for (Point p : pointList) {
g.addPoint(p);
}
for (EdgeWithWeight e : edgeList) {
g.addEdge(e);
}
return g;
}

/**
* 檢測是否產生迴路
若是存在迴路,則必存在一個子圖,是一個環路。環路中全部頂點的度>=2。
n算法:
第一步:刪除全部度<=1的頂點及相關的邊,並將另外與這些邊相關的其它頂點的度減一。
第二步:將度數變爲1的頂點排入隊列,並從該隊列中取出一個頂點重複步驟一。
若是最後還有未刪除頂點,則存在環,不然沒有環。
n算法分析:
因爲有m條邊,n個頂點。
i)若是m>=n,則根據圖論知識可直接判斷存在環路。(證實:若是沒有環路,則該圖必然是k棵樹 k>=1。根據樹的性質,邊的數目m = n-k。k>=1,因此:m<n)
ii)若是m<n 則按照上面的算法每刪除一個度爲0的頂點操做一次(最多n次),或每刪除一個度爲1的頂點(同時刪一條邊)操做一次(最多m次)。這兩種操做的總數不會超過m+n。因爲m<n,因此算法複雜度爲O(n)。
* @param this
* @return
*/
public boolean isGraphHasCircle() {
//若圖中沒有頂點,或者只有一個頂點則沒有迴路
if(this.getPointList() == null || this.getPointList().size() < 2){
return false;
}
if(this.getEdgeList().size() > this.getPointList().size()){
return true;
}

Graph g = this.clone();

int pointsLeftCount = g.getPointList().size();
while(pointsLeftCount > 0){
//一次遍歷如沒有刪除一個度小於2的點,則結束循環
boolean endFlag = true;
Point pointForRemove = null;
for (Point p : g.getPointList()) {
//計算頂點的度
if(g.getCountForPoint(p) <= 1){
//爲了規避最後一個頂點被刪除是的併發異常 採用標記刪除
pointForRemove = p;
//刪除以後重新遍歷頂點
endFlag = false;
break;
}
}
if(endFlag){
break;
}else{
g.removePoint(pointForRemove);
List<EdgeWithWeight> edgeForRemoveList = new ArrayList<EdgeWithWeight>();
for (EdgeWithWeight e : g.getEdgeList()) {
if(e.isPointInEdge(pointForRemove)){
edgeForRemoveList.add(e);
}
}
for (EdgeWithWeight edgeWithWeight : edgeForRemoveList) {
g.removeEdge(edgeWithWeight);
}
}
pointsLeftCount = g.getPointList().size();
}
if(g.getPointList().size() > 0){
return true;
}else{
return false;
}
}

/**
* 計算頂點的度
* @param p
* @return
*/
private int getCountForPoint(Point p) {
int count = 0;
for (EdgeWithWeight e : this.getEdgeList()) {
if(e.isPointInEdge(p)){
count = count + 1;
}
}
return count;
}

/**
* @param args
*/
public static void main(String[] args) {

}

}


[b]Prim算法調整[/b]

package com.zas.test.tree;


/**
* Prim算法實現的是找出一個有權重連通圖中的最小生成樹,即:具備最小權重且鏈接到全部結點的樹。
* @author zas
*/
public class Prim {
//一個要找最小生成樹的圖
Graph graph;

public Prim(Graph graph) {
super();
this.graph = graph;
}

public Graph getGraph() {
return graph;
}

public void setGraph(Graph graph) {
this.graph = graph;
}

/**
* 首先以一個結點做爲最小生成樹的初始結點,而後以迭代的方式找出與最小生成樹中各結點權重最小邊,
* 並加入到最小生成樹中。加入以後若是產生迴路則跳過這條邊,選擇下一個結點。
* 當全部結點都加入到最小生成樹中以後,就找出了連通圖中的最小生成樹了。
* @return
*/
public Graph prim() {
Graph minTree = new Graph();
for (Point p : graph.getPointList()) {
minTree.addPoint(p);
//得到該點的最小權重邊
EdgeWithWeight edge = getMinWeightEdgeForPoit(p, minTree);
if(null != edge){
//添加該邊到最小生成樹
minTree.addEdge(edge);
//檢測是否產生迴路
if(minTree.isGraphHasCircle()){
minTree.removeEdge(edge);
}
}
}
return minTree;
}


/**
* 獲取權重最小邊
* @param p
* @param minTree
* @return
*/
private EdgeWithWeight getMinWeightEdgeForPoit(Point p, Graph minTree) {
EdgeWithWeight e = null;
for (EdgeWithWeight edge : graph.getEdgeList()) {
if(!minTree.isEdgeInGraph(edge)){
if(edge.isPointInEdge(p)){
if(e == null){
e = edge;
}else{
if(e.compareTo(edge) == -1){
e = edge;
}
}
}
}
}
return e;
}

/**
* @param args
*/
public static void main(String[] args) {
//構建一個圖
Graph graph = new Graph();

Point a = new Point("A");
Point b = new Point("B");
Point c = new Point("C");
Point d = new Point("D");
Point e = new Point("E");
Point f = new Point("F");

graph.addPoint(a);
graph.addPoint(b);
graph.addPoint(c);
graph.addPoint(d);
graph.addPoint(e);
graph.addPoint(f);

//全部邊權重相同
graph.addEdge(new EdgeWithWeight(a, b, new Weight()));
graph.addEdge(new EdgeWithWeight(a, c, new Weight()));
graph.addEdge(new EdgeWithWeight(a, d, new Weight()));
graph.addEdge(new EdgeWithWeight(b, c, new Weight()));
graph.addEdge(new EdgeWithWeight(b, e, new Weight()));
graph.addEdge(new EdgeWithWeight(c, d, new Weight()));
graph.addEdge(new EdgeWithWeight(c, e, new Weight()));
graph.addEdge(new EdgeWithWeight(c, f, new Weight()));
graph.addEdge(new EdgeWithWeight(d, f, new Weight()));
graph.addEdge(new EdgeWithWeight(e, f, new Weight()));

Prim prim = new Prim(graph);
Graph minTree = prim.prim();
System.out.println(minTree);


Graph graphWithWeight = new Graph();

graphWithWeight.addPoint(a);
graphWithWeight.addPoint(b);
graphWithWeight.addPoint(c);
graphWithWeight.addPoint(d);
graphWithWeight.addPoint(e);
graphWithWeight.addPoint(f);

graphWithWeight.addEdge(new EdgeWithWeight(a, b, new Weight(6)));
graphWithWeight.addEdge(new EdgeWithWeight(a, c, new Weight(1)));
graphWithWeight.addEdge(new EdgeWithWeight(a, d, new Weight(5)));
graphWithWeight.addEdge(new EdgeWithWeight(b, c, new Weight(5)));
graphWithWeight.addEdge(new EdgeWithWeight(b, e, new Weight(3)));
graphWithWeight.addEdge(new EdgeWithWeight(c, d, new Weight(7)));
graphWithWeight.addEdge(new EdgeWithWeight(c, e, new Weight(5)));
graphWithWeight.addEdge(new EdgeWithWeight(c, f, new Weight(4)));
graphWithWeight.addEdge(new EdgeWithWeight(d, f, new Weight(2)));
graphWithWeight.addEdge(new EdgeWithWeight(e, f, new Weight(6)));

Prim primForWeigtTree = new Prim(graphWithWeight);
Graph minTreeForWeightTree = primForWeigtTree.prim();
System.out.println(minTreeForWeightTree);
}

}



[b]Kruskal算法實現[/b]
package com.zas.test.tree;import java.util.ArrayList;import java.util.List;/** * Kruskal算法思想不一樣於Prim算法, * Kruskal算法是一種按照連通網中邊的權值的遞增順序構造最小生成樹的算法。 * @author zas */public class Kruskal {	//一個要找最小生成樹的圖	Graph graph;	public Kruskal() {		super();	}	public Kruskal(Graph graph) {		super();		this.graph = graph;	}	public Graph getGraph() {		return graph;	}	public void setGraph(Graph graph) {		this.graph = graph;	}	/**	 * Kruskal算法的基本步驟 :	 * 假設G=(V,E)是一個具備n個頂點的連通網,T=(U,TE)是G的最小生成樹。	 * 令集合U的初值爲U=V,即包含有G中所有頂點,集合TE的初值爲TE={}。	 * 而後,將圖G中的邊按權值從小到大的順序依次選取,若選取的邊使生成樹T不造成迴路,則把它併入TE中,保留做爲T的一條邊;	 * 若選取的邊使生成樹T造成迴路,則將其捨棄,如此進行下去,直到TE中包含有n-1條邊爲止,此時的T即爲最小生成樹。	 * @return	 */	public Graph kruskal() {		Graph minTree = new Graph();		//將全部頂點加入最小生成樹		for (Point p : this.getGraph().getPointList()) {			minTree.addPoint(p);		}		//對原圖的邊按權值從小到大排序		List<EdgeWithWeight> edgeList = sortByEdgeAsc(this.graph.getEdgeList());		//加入 n - 1條最小權值邊		for (int i = 0; i < edgeList.size(); i++) {			EdgeWithWeight edge = edgeList.get(i);			minTree.addEdge(edge);			//是否含有環			if(minTree.isGraphHasCircle()){				minTree.removeEdge(edge);			}			//結束條件			if(minTree.getEdgeList().size() == minTree.getPointList().size() - 1){				break;			}		}		return minTree;	}	/**	 * 對邊按權值從小到大排序	 * @param edgeList	 * @return	 */	private List<EdgeWithWeight> sortByEdgeAsc(List<EdgeWithWeight> graphEdgeList) {		//克隆一下,防止影響到原圖		List<EdgeWithWeight> edgeList = new ArrayList<EdgeWithWeight>();		for (EdgeWithWeight edgeWithWeight : graphEdgeList) {			edgeList.add(edgeWithWeight);		}		//暫時採用選擇排序,若是數據規模大、有性能要求可改進		int selectedIndex = 0;		EdgeWithWeight edgeForSwap = null;		for (int i = 0; i < edgeList.size(); i++) {			selectedIndex = i;			for (int j = i + 1; j < edgeList.size(); j++) {				if(edgeList.get(j).compareTo(edgeList.get(selectedIndex)) == 1){					selectedIndex = j;				}			}			if(selectedIndex != i){				//交換				edgeForSwap = edgeList.get(selectedIndex);				edgeList.set(selectedIndex, edgeList.get(i));				edgeList.set(i, edgeForSwap);			}		}		return edgeList;	}	/**	 * @param args	 */	public static void main(String[] args) {		//構建一個圖		Graph graph = new Graph();		Point a = new Point("A");		Point b = new Point("B");		Point c = new Point("C");		Point d = new Point("D");		Point e = new Point("E");		Point f = new Point("F");		graph.addPoint(a);		graph.addPoint(b);		graph.addPoint(c);		graph.addPoint(d);		graph.addPoint(e);		graph.addPoint(f);		// 全部邊權重相同		graph.addEdge(new EdgeWithWeight(a, b, new Weight()));		graph.addEdge(new EdgeWithWeight(a, c, new Weight()));		graph.addEdge(new EdgeWithWeight(a, d, new Weight()));		graph.addEdge(new EdgeWithWeight(b, c, new Weight()));		graph.addEdge(new EdgeWithWeight(b, e, new Weight()));		graph.addEdge(new EdgeWithWeight(c, d, new Weight()));		graph.addEdge(new EdgeWithWeight(c, e, new Weight()));		graph.addEdge(new EdgeWithWeight(c, f, new Weight()));		graph.addEdge(new EdgeWithWeight(d, f, new Weight()));		graph.addEdge(new EdgeWithWeight(e, f, new Weight()));		Kruskal kruskal = new Kruskal(graph);		Graph minTree = kruskal.kruskal();		System.out.println(minTree);		Graph graphWithWeight = new Graph();		graphWithWeight.addPoint(a);		graphWithWeight.addPoint(b);		graphWithWeight.addPoint(c);		graphWithWeight.addPoint(d);		graphWithWeight.addPoint(e);		graphWithWeight.addPoint(f);		graphWithWeight.addEdge(new EdgeWithWeight(a, b, new Weight(6)));		graphWithWeight.addEdge(new EdgeWithWeight(a, c, new Weight(1)));		graphWithWeight.addEdge(new EdgeWithWeight(a, d, new Weight(5)));		graphWithWeight.addEdge(new EdgeWithWeight(b, c, new Weight(5)));		graphWithWeight.addEdge(new EdgeWithWeight(b, e, new Weight(3)));		graphWithWeight.addEdge(new EdgeWithWeight(c, d, new Weight(7)));		graphWithWeight.addEdge(new EdgeWithWeight(c, e, new Weight(5)));		graphWithWeight.addEdge(new EdgeWithWeight(c, f, new Weight(4)));		graphWithWeight.addEdge(new EdgeWithWeight(d, f, new Weight(2)));		graphWithWeight.addEdge(new EdgeWithWeight(e, f, new Weight(6)));		Kruskal kruskalForWeigtTree = new Kruskal(graphWithWeight);		Graph minTreeForWeightTree = kruskalForWeigtTree.kruskal();		System.out.println(minTreeForWeightTree);	}}
[b]測試用例所用的圖[/b] [img]http://dl.iteye.com/upload/attachment/482507/81eaa7e5-492e-3cc4-bdc4-062ae70f8a1e.jpg[/img] [b]總結[/b] Prim與Kruskal算法的關鍵都在於查找圖是否含有環。 Prim算法從點入手,再加入與其有關的不產生環路的最小權重邊。 Kruskal算法則先把全部點加入圖,再加入n-1條不產生環路的最小權重邊。 用面向對象的思惟實現兩個算法以後,感受記憶更深入了。