1. 程式人生 > >圖的操作集(java實現)

圖的操作集(java實現)

       本篇部落格主要介紹圖的建立、遍歷、最短路徑問題以及最小生成樹問題,包含了深度優先搜尋、廣度優先搜尋、Dijkstra演算法解決單源最短路徑問題、Floyd演算法解決多源最短路徑問題、Prim演算法和Kruskal解決最小生成樹問題。(詳細的說明一般喜歡嵌入到程式碼當中)

1、先看一下頂點型別

//圖的頂點型別
class Vertex {
	public char value;   //頂點值
	public boolean visited; //頂點是否被訪問過
	
	public Vertex(char v) {
		value=v;
		visited=false;
	}
}

2、然後再看邊的型別

//儲存邊的兩個頂點及邊的權值
class Edge{
	public int u;  //頂點
	public int v;  //頂點
	public int w; //權值
	public Edge(int u,int v,int w) {
		this.u=u;
		this.v=v;
		this.w=w;
	}
}

3、介紹了頂點和邊,就要說怎樣建立圖了。一般先建立一個沒有邊的圖,這裡用鄰接矩陣儲存頂點的權值,若沒有邊,則權值設定為無窮大。

//有權無向圖
public class Graph {
	private Vertex[] vertexList;  //存放頂點
	private int[][] mGraph;       //用矩陣表示邊
	private MyStack stack;        //棧
	private MyQueue queue;        //佇列
    private int size;             //當前頂點數
	private int[] distance;       //記錄到起點的距離
	private int[] path;           //記錄最短路徑經過的頂點
	//比如path[w]=v,表示從起點到頂點w需要先經過w的父頂點v
	private int[][] dist;  //dist[i][j]記錄i到j的最短距離
	private int[][] prev;  //prev[i][j]=k表示i到j的最短路徑會經過頂點k
	//並查集中指向父頂點的陣列
	int[] parent;
	
	public Graph(int maxSize) {
		vertexList=new Vertex[maxSize];
		mGraph=new int[maxSize][maxSize];
		stack=new MyStack(maxSize);
		queue=new MyQueue(maxSize);
		size=-1;
		distance=new int[maxSize];
		path=new int[maxSize];
		dist=new int[maxSize][maxSize];
		prev=new int[maxSize][maxSize];
		parent=new int[vertexList.length];
		init();  //初始化為沒有邊的圖
	}
	
	//初始化為沒有邊的圖
	public void init() {
		for(int i=0;i<mGraph.length;i++) {
			for(int j=0;j<mGraph[i].length;j++) {
				if(i==j) {
					mGraph[i][j]=0;   //自己到自己的權重為0
				}else {
					//還沒有邊的時候,權重初始化為無窮大
					mGraph[i][j]=Integer.MAX_VALUE; 
				}
			}
		}
	}
}

4、先圖中新增頂點和邊

	//新增頂點
	public boolean addVertex(char v) {
		if(size==vertexList.length) {
			System.out.println("滿了,無法新增頂點");
			return false;
		}
		vertexList[++size]=new Vertex(v);
		return true;
	}
	
	//新增邊
	public boolean addEdge(int start,int end,int weight) {
		if(start<0||start>=mGraph.length||end<0||end>=mGraph.length) {
			System.out.println("輸入的頂點序號不符合要求");
			return false;
		}
		mGraph[start][end]=weight;
		mGraph[end][start]=weight;
		return true;
	}
	

5、遍歷圖中的頂點(包括深度優先搜尋和廣度優先搜尋)

	//深度優先搜尋depthFirstSearch,有點類似於樹的先序遍歷
	public void DFS() {
		System.out.print(vertexList[0].value+" "); //訪問第一個頂點
		vertexList[0].visited=true;  //表示第一個頂點已經訪問過了
		stack.push(0);   //將第一個頂點入棧
		//當棧中還有元素
		while(!stack.isEmpty()) {
	        //找到棧當前頂點鄰接且未被訪問的頂點
			int v=getUnvisitedVertex(stack.peek());
			//如果當前頂點值為-1,則表示沒有鄰接且未被訪問頂點,那麼出棧頂點
            if(v == -1) {   
                stack.pop();
            }else { //否則訪問下一個鄰接頂點
                vertexList[v].visited = true;
                System.out.print(vertexList[v].value+" ");
                stack.push(v);
            }
		}
		
		//恢復visited為false,方便下次訪問
		for(int i=0;i<vertexList.length;i++) {
			vertexList[i].visited=false;
		}
		
	}
	
    //找到與某一頂點鄰接且未被訪問的頂點
    public int getUnvisitedVertex(int v) {
        for(int i = 0; i < mGraph.length; i++) {
            //v頂點與i頂點相鄰(鄰接矩陣值為1)且未被訪問 wasVisited==false
            if(v!=i && mGraph[v][i] < Integer.MAX_VALUE && vertexList[i].visited == false) {
                return i;
            }
        }
        return -1;
    }
    
    //廣度優先搜尋breadthFirstSearch,有點類似於樹的層次遍歷
    public void BFS() {
    	System.out.print(vertexList[0].value+" "); //訪問第一個頂點
    	vertexList[0].visited=true;  //標誌已經訪問過了
    	queue.add(0);  //將第一個頂點入隊
    	//當佇列不為空
    	while(!queue.isEmpty()) {
    		//尋找當前頂點沒有訪問的鄰接點
    		int v=getUnvisitedVertex(queue.peek());
    		if(v!=-1) {
    			System.out.print(vertexList[v].value+" ");
    			vertexList[v].visited=true;
    			queue.add(v);
    		}else {
    			queue.remove();
    		}
    	}
    	
		//恢復visited為false,方便下次訪問
		for(int i=0;i<vertexList.length;i++) {
			vertexList[i].visited=false;
		}
    }

6、Dijkstra解決單源最短路徑問題

//Dijkstra解決單源最短路徑問題
  //它的主要特點是以起始點為中心向外層層擴充套件(廣度優先搜尋思想),直到擴充套件到終點為止
  //s表示的是以第幾個頂點為起點,從0開始
    public void dijkstra(int s) {
    	//初始化距離和路徑
    	for(int i=0;i<vertexList.length;i++) {
    		distance[i]=Integer.MAX_VALUE; //到起點的距離設定為無窮大
    		path[i]=-1;  //到起點的路徑初始化為-1
    	}
    	distance[s]=0;  //到起點本身的距離為0   	
    	//每次取最小值,其實可以用最小堆來實現,這裡就直接比較了
    	//進行n次迴圈
    	for(int i=0;i<vertexList.length;i++) {
    		int k=0;  //記錄最小路徑的頂點序號
        	int min=Integer.MAX_VALUE;
        	//從未被訪問過的頂點中找一個距離最小的頂點
    		for(int j=0;j<vertexList.length;j++) {
    			//如果還沒有訪問過,並且比當前值要小
    			if(vertexList[j].visited==false && distance[j]<min) {
    				vertexList[j].visited=true; //設定為已訪問
    				min=distance[j];      	//更新最小值
    				k=j;    //記錄最小路徑的頂點序號
    			}
    		}
            // 修正當前最短路徑和前驅頂點
            // 即,當已知"頂點k的最短路徑"之後,更新"未獲取最短路徑的頂點的最短路徑和前驅頂點"。
            for (int j = 0; j < vertexList.length; j++) {
            	//找頂點k的鄰接點j,並更新它的鄰接點到起點的最短路徑
                int tmp = (mGraph[k][j]==Integer.MAX_VALUE ? Integer.MAX_VALUE : (min + mGraph[k][j]));
                //min + mGraph[k][j]就表示頂點k的最短路徑加上<k,j>邊的權重,即為頂點j可能的最短路徑
                if (vertexList[j].visited==false && (tmp<distance[j]) ) {
                    distance[j] = tmp;  //更新頂點j到起點的最短路徑
                    path[j] = k;        //設定它的父頂點為k
                }
            }
    	}

        // 列印Dijkstra最短路徑的結果
    	printDijkstra(s);
    	
		//恢復visited為false,方便下次訪問
		for(int i=0;i<vertexList.length;i++) {
			vertexList[i].visited=false;
		}
  }    
    // 列印Dijkstra最短路徑的結果
    public void printDijkstra(int s) {
    	//利用棧後進先出的特性,將路徑逆序
    	MyStack st=new MyStack(vertexList.length);
        System.out.printf("dijkstra(%c): \n", vertexList[s].value);
        for (int i=0; i < vertexList.length; i++) {
            System.out.printf("  shortest(%c, %c)=%d 路徑為:", vertexList[s].value, vertexList[i].value, distance[i]);
            //這裡可以用一個棧來儲存頂點,然後出棧就是順序輸出了,而不是反向輸出
            //列印路徑
            st.push(i);  //終點
            int tmp=path[i];
            while(tmp!=-1) {
            	st.push(tmp);
            	tmp=path[tmp];
            }
        	while(!st.isEmpty()) {
        		System.out.printf("%c-->",vertexList[st.pop()].value);
        	}
            System.out.println();
        }
    }
  

7、Floyd演算法求解任意兩個頂點的最短距離問題,也就是多源最短路徑問題

    //Floyd演算法求解任意兩個頂點的最短距離問題,也就是多源最短路徑問題
    public void floyd() {
    	//初始化
    	System.out.println("初始化的值:");
    	for(int i=0;i<vertexList.length;i++) {
    		for(int j=0;j<vertexList.length;j++) {
    			dist[i][j]=mGraph[i][j];  //儲存的是權值
    			prev[i][j]=j;   //i到j一定會經過j
    		}
    	}
    	//三重迴圈,最外層的是頂點的個數,中間兩層是遍歷整個矩陣
    	//思想是:當k=0時,就藉助於第k個頂點,如果i到j的距離可以變小,則更新最小距離
    	//其實就是藉助於前k個頂點,如果i到j的距離可以變小,則更新最小距離
    	for(int k=0;k<vertexList.length;k++) {
    		for(int i=0;i<vertexList.length;i++) {
    			for(int j=0;j<vertexList.length;j++) {
                    // 如果經過下標為k頂點路徑比原兩點間路徑更短,則更新dist[i][j]和prev[i][j]
                    int tmp = (dist[i][k]==Integer.MAX_VALUE || dist[k][j]==Integer.MAX_VALUE) ? Integer.MAX_VALUE : (dist[i][k] + dist[k][j]);
                    if (dist[i][j] > tmp) {
                        // "i到j最短路徑"對應的值設,為更小的一個(即經過k)
                        dist[i][j] = tmp;
                        // "i到j最短路徑"對應的路徑,經過k
                        prev[i][j] = prev[i][k];
                    }
    			}
    		}
    	}
    	
    	// 列印floyd最短路徑的結果
        System.out.printf("floyd: \n");
        for (int i = 0; i < vertexList.length; i++) {
            for (int j = 0; j < vertexList.length; j++)
                System.out.printf("%2d  ", dist[i][j]);
            System.out.printf("\n");
        }
    }

8、Prim演算法解決最小生成樹問題,以頂點為思考物件

    //Prim演算法解決最小生成樹問題,以頂點為思考物件
    public void prim(int start) {
    	int[] prims=new int[vertexList.length]; //記錄最小生成樹的頂點序號
    	//初始化
    	for(int i=0;i<vertexList.length;i++) {
    		distance[i]=mGraph[start][i];  //到起點的權值
    		prims[i]=-1;
    	}
    	distance[start]=0;  //自己到自己的距離為0
    	int index=0;  //最小生成樹的索引
    	prims[index++]=start;
    //	vertexList[start].visited=true;  //表示已經在最小生成樹中
    	
    	for(int i=0;i<vertexList.length;i++) {
    		if(i==start) {
    			continue;
    		}
    		int min=Integer.MAX_VALUE;
    		int k=-1;
    		for(int j=0;j<vertexList.length;j++) {
    			//distance[j]==0,表示已經在最小生成樹中
    			//如果不在最小生成樹中,並且與最小生成樹中的某個頂點組成的邊的權值更小
    			if(distance[j]!=0 && distance[j]<min) {
    				min=distance[j]; 
    				k=j;
    			}
    		}
    		
    		//迴圈結束後,第k個頂點就是和已經收錄的頂點構成邊權值最小的頂點
    		prims[index++]=k;
    		//vertexList[k].visited=true;
    		distance[k]=0;  //表示已經在最小生成樹中
    		//更新第k個頂點到未被收錄進最小生成樹中鄰接點的權值
    		for(int j=0;j<vertexList.length;j++) {
    			//如果j是k的未被收錄的鄰接點
    			if(distance[j]!=0 && mGraph[k][j]+distance[k]<distance[j]) {
    				distance[j]=mGraph[k][j]+distance[k];
    			}
    		}
    	}
    	
    	//列印最小生成樹
    	for(int i=0;i<vertexList.length;i++) {
    		  System.out.print(vertexList[prims[i]].value+" ");
    	}
    	//列印最小權值:從最小生成樹的第二個頂點開始,找它到前驅頂點的最小權值
    	int sum=0;
    	//一共n-1條邊
    	for(int i=1;i<vertexList.length;i++) {
    		int min=Integer.MAX_VALUE;
    		for(int j=0;j<i;j++) {
    			if(mGraph[prims[j]][prims[i]]<min) {
    				//prims[j]表示已經在最小生成樹中的頂點
    				min=mGraph[prims[j]][prims[i]];
    			}
    		}
    		sum+=min;
    	}
    	System.out.println("最小權值和為:"+sum);
    }
    

9、Kruskal演算法:求最小生成樹

    //Kruskal演算法:求最小生成樹
    public void kruskal() {
    	ArrayList<Edge> list=new ArrayList<>();
    	//初始化邊
    	for(int i=0;i<vertexList.length;i++) {
    		for(int j=0;j<vertexList.length;j++) {
    			//如果兩個頂點有邊
    			if(mGraph[i][j]!=0 && mGraph[i][j]<Integer.MAX_VALUE) {
    				list.add(new Edge(i,j,mGraph[i][j]));
    			}
    		}
    	}
    	//對邊按權值排序   	
    	Collections.sort(list, new Comparator<Edge>() {

			@Override
			public int compare(Edge o1, Edge o2) {
				return o1.w-o2.w; //權值小的在前
			}
    	});
    	//初始化並查集,parent[i]=-1;表示這棵樹只有它自己,一開始是n棵樹
    	for(int i=0;i<parent.length;i++) {
    		parent[i]=-1;
    	}
    	//下面才是kruskal演算法
    	//list.size()就是邊的數量
    	int u,v,num=0,sum=0,index=0;  
    	char[] result=new char[2*vertexList.length-2]; //記錄結果的陣列,邊的順序
    	System.out.println("下面是kruskal演算法:");
    	for(int i=0;i<list.size();i++) {
    		Edge e=list.get(i);
    		u=e.u;
    		v=e.v;
    		//如果頂點不屬於同一個集合
    		if(findRoot(u)!=findRoot(v)) {
    			sum+=e.w;
    			result[index++]=vertexList[u].value;
    			result[index++]=vertexList[v].value;
    			num++;
    			union(u, v);
    		}
			//如果有n-1條邊,就退出了
			if(num==vertexList.length-1) {
				break;
			}
    	}
    	//列印邊的資訊
    	System.out.println("kruskal包括的邊依次是:");
    	for(int i=0;i<result.length;i+=2) {
    		System.out.println(result[i]+"--"+result[i+1]);
    	}
    	System.out.println("kruskal的最小權值:"+sum);
    }
    
    //查詢某個頂點屬於哪個集合
    public int findRoot(int v) {
    	int root;  //集合的根節點
    	for(root=v;parent[root]>=0;root=parent[root]);
    	//路徑壓縮
    	while(root!=v) {
    		int tmp=parent[v];
    		parent[v]=root;
    		v=tmp;
    	}
    	return root;
    }
    
  //將兩個不同集合的元素進行合併,使兩個集合中任兩個元素都連通
    void union( int u, int v)
    {
        int r1 = findRoot(u), r2 = findRoot(v); //r1 為 u 的根結點,r2 為 v 的根結點
        int tmp = parent[r1] + parent[r2]; //兩個集合結點個數之和(負數)
        //如果 R2 所在樹結點個數 > R1 所在樹結點個數(注意 parent[r1]是負數)
        if( parent[r1] > parent[r2] ) //優化方案――加權法則
        {
            parent[r1] = r2; 
            parent[r2] = tmp;
        }
        else
        {
            parent[r2] = r1; 
            parent[r1] = tmp;
        }
    }

10、測試各個部分

	public static void main(String[] args) {
		Graph g=new Graph(7);
		g.addVertex('A');
		g.addVertex('B');
		g.addVertex('C');
		g.addVertex('D');
		g.addVertex('E');
		g.addVertex('F');
		g.addVertex('G');
		
		//下面是邊的關係,有邊的值為權重,無邊的值為無窮大
//        int matrix[][] = {
//                /*A*//*B*//*C*//*D*//*E*//*F*//*G*/
//         /*A*/ {   0,  12, INF, INF, INF,  16,  14},
//         /*B*/ {  12,   0,  10, INF, INF,   7, INF},
//         /*C*/ { INF,  10,   0,   3,   5,   6, INF},
//         /*D*/ { INF, INF,   3,   0,   4, INF, INF},
//         /*E*/ { INF, INF,   5,   4,   0,   2,   8},
//         /*F*/ {  16,   7,   6, INF,   2,   0,   9},
//         /*G*/ {  14, INF, INF, INF,   8,   9,   0}};
		g.addEdge(0, 1,12);  //AB相連
		g.addEdge(0, 5, 16); //AF
		g.addEdge(0, 6, 14); //AG
		g.addEdge(1, 2,10);   //BC相連
		g.addEdge(1, 5, 7);   //BF
		g.addEdge(2, 3,3); //CD相連
		g.addEdge(2, 4,5); //CE相連
		g.addEdge(2, 5, 6); //CF
		g.addEdge(3, 4, 4); //DE
		g.addEdge(4, 5, 2); //EF
		g.addEdge(4, 6, 8); //EG
		g.addEdge(5, 6, 9); //FG
		
		//深度優先訪問
		System.out.println("深度優先:");
		g.DFS();
		System.out.println();
		//廣度優先搜尋
		System.out.println("廣度優先:");
		g.BFS();
		System.out.println();
		
		//Dijkstra演算法
		g.dijkstra(3);
		System.out.println("路徑:");
		for(int i=0;i<g.vertexList.length;i++) {
			System.out.print(g.path[i]+" ");
		}
		
		//Floyd演算法
		System.out.println();
		g.floyd();	
		
		//最小生成樹:prim演算法
		System.out.println("最小生成樹:prim演算法");
		g.prim(0);
		
		//最小生成樹:kruskal演算法
		g.kruskal();
	}