1. 程式人生 > >圖的常用操作(鄰接表,java實現)

圖的常用操作(鄰接表,java實現)

之前寫過圖的鄰接矩陣表示及其常用操作https://blog.csdn.net/qiuxinfa123/article/details/83719789,這篇部落格主要介紹鄰接表的相關操作,包括圖的建立、深度優先搜尋、廣度優先搜尋、單源最短路徑、多源最短路徑、最小生成樹的Prim和Kruskal演算法。

先看下節點型別以及邊的型別。

	//作為某個點的鄰接點的頂點資訊
	class Node{
		int index;  //頂點的序號
		int weight;  //以該頂點為終點的邊的權值
		Node nextNode; //指向下一個頂點
	}
	
	//輸入的所有頂點的型別,是任意一條連結串列的起點
	class Vertex{
		char data;        //頂點值
		Node firstEdge;  //指向第一條邊
	}
	
	//邊的型別
	static class Edge{
		char start;  //起點
		char end;    //終點
		int weight;  //邊的權值
		public Edge(char start,char end,int weight) {
			this.start=start;
			this.end=end;
			this.weight=weight;
		}
	}

然後看圖的初始化。

	//有參構造器
	public LinkGraph(char[] vert,Edge[] edge) {
		//讀入頂點,並初始化
		vertex=new Vertex[vert.length];
		parent=new int[vert.length];
		for(int i=0;i<vertex.length;i++) {
			vertex[i]=new Vertex();
			vertex[i].data=vert[i];       //頂點值
			vertex[i].firstEdge=null;  //還沒有鄰接點,當然沒有邊了
		}
		//初始化邊
		for(int i=0;i<edge.length;i++) {
			char start=edge[i].start;
			char end=edge[i].end;
			//獲取頂點對應的序號
			int p1=getPosition(start);
			int p2=getPosition(end);
			//1.把p2連線在以p1為頭的連結串列中
			Node node1=new Node();
			node1.index=p2;
			node1.weight=edge[i].weight;
			linkedLast(p1,node1);
			//2.因為是無向圖,所以還需要把p1連線在以p2為頭的連結串列中
			Node node2=new Node();
			node2.index=p1;
			node2.weight=edge[i].weight;
			linkedLast(p2,node2);
		}
	}

獲取某個頂點的序號以及將某個頂點插入到指定頂點的後面。

	//獲取某個頂點對應的序號
	public int getPosition(char v) {
		for(int i=0;i<vertex.length;i++) {
			if(vertex[i].data==v) {
				return i;
			}
		}
		//不存在這樣的頂點則返回-1
		return -1;
	}
	
	//尾插法,將頂點連線到連結串列的尾巴
	public void linkedLast(int index,Node node) {
		if(vertex[index].firstEdge==null) {
			vertex[index].firstEdge=node;
		}else {
			Node tmp=vertex[index].firstEdge;
			while(tmp.nextNode!=null) {
				tmp=tmp.nextNode;
			}
			tmp.nextNode=node;
		}
	}

列印圖的資訊。

	//列印圖
	public void print() {
		System.out.println("鄰接表儲存的圖:");
		for(int i=0;i<vertex.length;i++) {
			System.out.print(vertex[i].data+"-->");
			//如果存在鄰接點
			Node tmp=vertex[i].firstEdge;
			while(tmp.nextNode!=null) {
				System.out.print(vertex[tmp.index].data+"-->");
				tmp=tmp.nextNode;
			}
			System.out.print(vertex[tmp.index].data);
			System.out.println();
		}
	}

圖的遍歷,包括DFS和BFS。

	//深度優先搜尋,從第一個頂點開始遍歷
	public void DFS() {
		boolean[] visited=new boolean[vertex.length];  //預設為false;
		int[] path=new int[vertex.length];  //記錄遍歷的頂點序號
		int index=0;  //path[]的索引
		MyStack stack=new MyStack(vertex.length);
		visited[0]=true;
		stack.push(0);
		path[index++]=0;
		while(!stack.isEmpty()) {
			int v=getUnVisitedAdjVertex(stack.peek(),visited);
			//如果不存在沒有訪問的鄰接點,就出棧,原路返回
			if(v==-1) {
				stack.pop();
			}
			//否則,存在還沒有訪問過的鄰接點,入棧,並標註已訪問
			else {
				path[index++]=v;  //訪問鄰接點
				visited[v]=true;  //標誌已訪問
				stack.push(v);    //入棧
			}
		}
		
		//列印DFS路徑
		System.out.println("DFS路徑:");
		for(int i=0;i<path.length;i++) {
			System.out.print(vertex[path[i]].data+" ");
		}
	}
	
	//查詢某個點的還沒有被訪問的鄰接點的序號
	public int getUnVisitedAdjVertex(int v,boolean[] visited) {
		Node tmp=vertex[v].firstEdge;
		//如果存在鄰接點
		while(tmp!=null) {
			//並且鄰接點還沒有訪問過,就返回該鄰接點的序號
			if(visited[tmp.index]==false) {
				return tmp.index;
			}
			tmp=tmp.nextNode;
		}
		//不存在沒有被訪問的鄰接點
		return -1;
	}
	
	//廣度優先搜尋,從第一個頂點開始遍歷
	public void BFS() {
		boolean[] visited=new boolean[vertex.length];  //預設為false;
		int[] path=new int[vertex.length];  //記錄遍歷的頂點序號
		int index=0;  //path[]的索引
		MyQueue queue=new MyQueue(vertex.length);
		visited[0]=true;
		queue.add(0);
		path[index++]=0;
		while(!queue.isEmpty()) {
			int v=getUnVisitedAdjVertex(queue.peek(), visited);
			//如果不存在沒有訪問的鄰接點,就出隊
			if(v==-1) {
				queue.remove();
			}
			//否則,存在還沒有訪問過的鄰接點,入隊,並標註已訪問
			else {
				path[index++]=v;  //訪問鄰接點
				visited[v]=true;  //標誌已訪問
				queue.add(v);     //入隊
			}
		}
		
		//列印BFS路徑
		System.out.println("BFS路徑:");
		for(int i=0;i<path.length;i++) {
			System.out.print(vertex[path[i]].data+" ");
		}		
	}

單源最短路徑問題的Dijkstra演算法。

	//單源最短路徑問題:Dijkstra演算法,s為起點,比上一種方法比較容易計算最短距離
	public void dijkstra(int s) {
		int[] path=new int[vertex.length];  //記錄到起點經過的頂點路徑
		int[] distance=new int[vertex.length];  //記錄到起點的距離
		boolean[] visited=new boolean[vertex.length]; //標記是否訪問過
		//初始化到起點的距離
		for(int i=0;i<vertex.length;i++) {
			distance[i]=getWeight(s, i);
			if(i!=s && distance[i]<INF) {
				path[i]=s;
			}else {
				path[i]=-1;
			}
		}
		visited[s]=true;  //起點已經訪問過了
		//遍歷所有頂點,並更新到起點的距離
		for(int i=0;i<vertex.length;i++) {
			if(i==s) {
				continue;
			}
			int min=INF;
			int k=-1;
			//找到距離起點距離最短的頂點
			//distance[j]=0,表示已經訪問過了
			for(int j=0;j<vertex.length;j++) {
				if(visited[j]==false && distance[j]<min) {
					min=distance[j];
					k=j;
				}
			}
			//for迴圈結束後,k就是要找的那個頂點
			visited[k]=true;  //表示第k個頂點已經訪問過了
			//更新頂點k的鄰接點到起點的最小距離
			for(int j=0;j<vertex.length;j++) {
				//如果不是k的鄰接點
				if(getWeight(k, j)==INF) {
					continue;
				}
				int tmp=distance[k]+getWeight(k, j);
				//如果是未被訪問過的鄰接點,則更新其到起點的距離
				if(visited[j]==false && distance[j]>tmp) {
					distance[j]=tmp;
					path[j]=k;  //這裡的意思是,頂點j到達起點,必定經過頂點k
				}
			}
			
		}
		
		//列印Dijkstra演算法的最短路徑,這裡的路徑是逆序輸出的,可以使用棧將其恢復正常
		System.out.printf("Dijkstra(%c)\n",vertex[s].data);
		for(int i=0;i<vertex.length;i++) {
		//	System.out.print(vertex[s].data+"到"+vertex[i].data+"的最短路徑為:");
			System.out.print(path[i]+" ");
			int tmp=i;
			//tmp=1時,就到了起點了
			while(tmp!=-1) {
				System.out.print(vertex[tmp].data+"<--");
				tmp=path[tmp];
			}
			System.out.print("    最小權值為:"+distance[i]);
			System.out.println();  //換行
		}
	}
	
    //獲取邊<start, end>的權值;若start和end不是連通的,則返回無窮大。    
    private int getWeight(int start, int end) {
        if (start==end)
            return 0;
        Node node = vertex[start].firstEdge;
        while (node!=null) {
            if (end==node.index)
                return node.weight;
            node = node.nextNode;
        }

        return INF;
    }

多源最短路徑問題的Floyd演算法。

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

最小生成樹的Prim演算法。

   //最小生成樹:Prim演算法
    public void prim(int s) {
		int[] distance=new int[vertex.length];  //記錄到起點的距離
		//初始化到起點的距離
		for(int i=0;i<vertex.length;i++) {
			distance[i]=getWeight(s, i);  //起點到頂點i的權值
		}
    	int[] prims=new int[vertex.length];  //記錄訪問的頂點序號
    	int index=0;  //prims[]的索引
    	prims[index++]=s;  //第一個訪問的是起點s
    	
		//遍歷所有頂點,並更新到起點的距離
		for(int i=0;i<vertex.length;i++) {
			if(i==s) {
				continue;
			}
			int min=INF;
			int k=-1;
			//找到距離起點距離最短的頂點
			//distance[j]=0,表示已經訪問過了
			for(int j=0;j<vertex.length;j++) {
				if(distance[j]!=0 && distance[j]<min) {
					min=distance[j];
					k=j;
				}
			}
			//for迴圈結束後,k就是要找的那個頂點
			prims[index++]=k; 
			distance[k]=0;  //表示第k個頂點已經訪問過了
			//更新頂點k的鄰接點到起點的最小距離
			for(int j=0;j<vertex.length;j++) {
				//如果不是k的鄰接點
				if(getWeight(k, j)==INF) {
					continue;
				}
				int tmp=distance[k]+getWeight(k, j);
				//如果是未被訪問過的鄰接點,則更新其到起點的距離
				if(distance[j]!=0 && distance[j]>tmp) {
					distance[j]=tmp;
				}
			}
			
		}
    	
		//列印最小生成樹
		System.out.printf("prim(%c)\n",vertex[s].data);
		for(int i=0;i<vertex.length-1;i++) {
			System.out.print(vertex[prims[i]].data+"-->");
		}
		System.out.print(vertex[prims[vertex.length-1]].data);
		int sum=0; //最小權值和
		for(int i=1;i<vertex.length;i++) {
			int min=INF;
			for(int j=0;j<i;j++) {
				if(getWeight(prims[i], prims[j])<min) {
					min=getWeight(prims[i], prims[j]);
				}
			}
			sum+=min;
		}
		System.out.print("最小權值和為:"+sum);
		System.out.println();
    }
    
    //判斷v是不是u的鄰接點
    public boolean getAdjVertex(int u,int v) {
    	Node tmp=vertex[u].firstEdge;
    	while(tmp!=null) {
    		if(tmp.index==v) {
    			return true;
    		}
    		tmp=tmp.nextNode;
    	}
    	return false;
    }
    

最小生成樹的Krustral演算法。

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

			@Override
			public int compare(Edge o1, Edge o2) {
				return o1.weight-o2.weight; //權值小的在前
			}
    	});
    	//初始化並查集,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*vertex.length-2]; //記錄結果的陣列,邊的順序
    	System.out.println("下面是kruskal演算法:");
    	for(int i=0;i<list.size();i++) {
    		Edge e=list.get(i);
    		u=e.start-65;  //將字元轉換為整數下標
    		v=e.end-65;
    		//如果頂點不屬於同一個集合
    		if(findRoot(u)!=findRoot(v)) {
    			sum+=e.weight;
    			result[index++]=vertex[u].data;
    			result[index++]=vertex[v].data;
    			num++;
    			union(u, v);
    		}
			//如果有n-1條邊,就退出了
			if(num==vertex.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;
        }
    }

順便貼上測試資料。

	public static void main(String[] args) {
        char[] vexs = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};   //頂點    
        Edge[] edges = {                                   //邊
                   // 起點      終點    權
            new Edge('A', 'B', 12), 
            new Edge('A', 'F', 16), 
            new Edge('A', 'G', 14), 
            new Edge('B', 'C', 10), 
            new Edge('B', 'F',  7), 
            new Edge('C', 'D',  3), 
            new Edge('C', 'E',  5), 
            new Edge('C', 'F',  6), 
            new Edge('D', 'E',  4), 
            new Edge('E', 'F',  2), 
            new Edge('E', 'G',  8), 
            new Edge('F', 'G',  9), 
        };
        
        LinkGraph graph=new LinkGraph(vexs,edges);
        //列印圖的鄰接表
        graph.print();       
        //深度優先搜尋
        graph.DFS();
        System.out.println();
        //廣度優先搜尋
        graph.BFS();
        System.out.println();
        //單源最短路徑:Dijkstra演算法
        System.out.println("單源最短路徑:Dijkstra演算法");
        graph.dijkstra(3); 
        System.out.println();
        //多源最短路徑問題:Floyd演算法
        graph.floyd();
        System.out.println();
        //最小生成樹:Prim演算法
        graph.prim(0);
        //最小生成樹:Kruskal演算法
        graph.kruskal();
	}