java 数据结构 图
以下内容主要来自大话数据结构之中,部分内容参考互联网中其他前辈的博客,主要是在自己理解的基础上进行记录。
图的定义
图是由顶点的有穷非空集合和顶点之间边的集合组成,通过表示为G(V,E),其中,G标示一个图,V是图G中顶点的集合,E是图G中边的集合。
无边图:若顶点Vi到Vj之间的边没有方向,则称这条边为无项边(Edge),用序偶对(Vi,Vj)标示。
有向图:若从顶点Vi到Vj的边是有方向的,则成这条边为有向边,也称为弧(Arc)。用有序对(Vi,Vj)标示,Vi称为弧尾,Vj称为弧头。如果任意两条边之间都是有向的,则称该图为有向图。
有向图G2中,G2=(V2,{E2}),顶点集合(A,B,C,D),弧集合E2={<A,D>,{B,A},<C,A>,<B,C>}.
权(Weight):有些图的边和弧有相关的数,这个数叫做权(Weight)。这些带权的图通常称为网(Network)。
图的存储结构
图的存储结构一般分为邻接矩阵和十字链表
邻接矩阵:图的邻接矩阵存储方式是用两个数组来标示图。一个一位数组存储图顶点的信息,一个二维数组(称为邻接矩阵)存储图中边或者弧的信息。
设图G有n个顶点,则邻接矩阵是一个n*n的方阵,定义为:
十字链表:
顶点表结点结构:
firstin:表示入边表头指针,指向该顶点的入边表中第一个结点。
firstout:表示出边表头指针,指向该顶点的出边表中的第一个结点。
边表结点结构:
tailvex:指弧起点在顶点表的下标。
headvex:指弧终点在顶点表中的下标。
headlink:指入边表指针域。
taillink:指边表指针域。
如果是网,还可以再增加一个weight域来存储权值。
蓝线表示出度,红线表示入度
十字链表的优点:
十字链表是把邻接表和逆邻接表整合在一起,这样既容易找到以Vi为尾的弧,也容易找到以Vi为头的弧,
因而容易求的顶点的出度和入度。
图的搜索:
深度优先遍历:也有称为深度优先搜索,简称DFS。其实,就像是一棵树的前序遍历。它从图中某个结点v出发,访问此顶点,然后从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有 路径相通的顶点都被访问到。若图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中的所有顶点都被访问到为止。
基本实现思想:
(1)访问顶点v;
(2)从v的未被访问的邻接点中选取一个顶点w,从w出发进行深度优先遍历;
(3)重复上述两步,直至图中所有和v有路径相通的顶点都被访问到。
广度优先遍历:也称广度优先搜索,简称BFS。BFS算法是一个分层搜索的过程,和树的层序遍历算法类同,它也需要一个队列以保持遍历过的顶点顺序,以便按出队的顺序再去访问这些顶点的邻接顶点。
基本实现思想:
(1)顶点v入队列。
(2)当队列非空时则继续执行,否则算法结束。
(3)出队列取得队头顶点v;访问顶点v并标记顶点v已被访问。
(4)查找顶点v的第一个邻接顶点col。
(5)若v的邻接顶点col未被访问过的,则col入队列。
(6)继续查找顶点v的另一个新的邻接顶点col,转到步骤(5)。
直到顶点v的所有未被访问过的邻接点处理完。转到步骤(2)。
广度优先遍历图是以顶点v为起始点,由近至远,依次访问和v有路径相通而且路径长度为1,2,……的顶点。为了使“先被访问顶点的邻接点”先于“后被访问顶点的邻接点”被访问,需设置队列存储访问的顶点。
最小生成树
我们把构造连通网的最小代价生成的树称为最小生成树,即权值最小的生成树。
实现方式:
1、普利姆算法(Prim)
基本思想:假设G=(V,E)是连通的,TE是G上最小生成树中边的集合。算法从U={u0}(u0∈V)、TE={}开始。重复执行下列操作:
在所有u∈U,v∈V-U的边(u,v)∈E中找一条权值最小的边(u0,v0)并入集合TE中,同时v0并入U,直到V=U为止。
此时,TE中必有n-1条边,T=(V,TE)为G的最小生成树。
Prim算法的核心:始终保持TE中的边集构成一棵生成树。
注意:prim算法适合稠密图,其时间复杂度为O(n^2),其时间复杂度与边得数目无关,而kruskal算法的时间复杂度为O(eloge)跟边的数目有关,适合稀疏图。
示例:
(1)图中有6个顶点v1-v6,每条边的边权值都在图上;在进行prim算法时,我先随意选择一个顶点作为起始点,当然我们一般选择v1作为起始点,好,现在我们设U集合为当前所找到最小生成树里面的 顶点,TE集合为所找到的边,现在状态如下:
U={v1}; TE={};
(2)现在查找一个顶点在U集合中,另一个顶点在V-U集合中的最小权值,如下图,在红线相交的线上找最小值。
通过图中我们可以看到边v1-v3的权值最小为1,那么将v3加入到U集合,(v1,v3)加入到TE,状态如下:
U={v1,v3}; TE={(v1,v3)};
(3)继续寻找,现在状态为U={v1,v3}; TE={(v1,v3)};在与红线相交的边上查找最小值。
我们可以找到最小的权值为(v3,v6)=4,那么我们将v6加入到U集合,并将最小边加入到TE集合,那么加入后状态如下:
U={v1,v3,v6}; TE={(v1,v3),(v3,v6)}; 如此循环一下直到找到所有顶点为止。
(4)下图像我们展示了全部的查找过程:
2、克鲁斯卡尔算法(Kruskal)
假设连通网N=(V,{E})。则令最小生成树的初始状态为只有n个顶点而无边的非连通图T=(V,{}),图中每个顶点自成一个连通分量。在E中选择最小代价的边,若该边依附的顶点落在T中不同的连通分 量中,则将该边加入到T中,否则舍去此边而选择下一条代价最小的边,依次类推,直到T中所有顶点都在同一连通分量上为止。
示例如下:
图中先将每个顶点看作独立的子图,然后查找最小权值边,这条边是有限制条件的,边得两个顶点必须不在同一个图中,如上图,第一个图中找到最小权值边为(v1,v3),且满足限制条件,继续查找边 (v4,v6),(v2,v5),(v3,v6),当查找到最后一条边时,仅仅只有(v2,v3)满足限制条件,其他的如(v3,v4),(v1,v4)都在一个子图里面,不满足条件,至此已经找到最小生成树的
所有边。
上述所有的具体代码实现:
1 /** 2 * java数据结构无向图的实现 3 * 2016/4/29 4 */ 5 package cn.Link; 6 7 import java.util.ArrayList; 8 import java.util.LinkedList; 9 import java.util.Queue; 10 public class Graph { 11 12 final static int MAX = 65535; //两个定点之间没有路径时的长度 13 int verticts; //顶点数 14 int sides; //顶点为verticts的连通图的边数值 15 int[][] arc;//存储图的二维数组 16 String[] vex; 17 Graph(int verticts){ 18 this.verticts = verticts; 19 this.sides = 0; 20 for(int i = verticts-1;i>0;i--){ 21 this.sides +=i; //获得连通图的边数值 22 } 23 this.arc = new int[verticts][verticts]; 24 this.vex = new String[verticts]; 25 //初始化一个有向图,所有边设为最大权值(即不可能达到的值) 26 for(int i = 0; i < verticts;i++){ 27 for(int j = 0; j < verticts;j++){ 28 if(i!=j){ 29 this.arc[i][j] = MAX; 30 }else{ 31 this.arc[i][j]=0; 32 } 33 34 } 35 } 36 } 37 38 //假设有这样一个图 39 public void addGraph(){ 40 //顶点数据 41 this.vex[0] = "beijing"; 42 this.vex[1] = "shanghai"; 43 this.vex[2] = "tianjing"; 44 this.vex[3] = "chengdu"; 45 this.vex[4] = "changsha"; 46 this.vex[5] = "chongqing"; 47 48 //边的权值 49 for(int i = 1; i < verticts;i++){ 50 for(int j = 0; j < i;j++){ 51 int n = (int)(Math.random()*100); //随机生成权值 52 if(n > 0){ 53 this.arc[i][j]=this.arc[j][i] = n; 54 }else if(n == 0){ 55 this.arc[i][j]=this.arc[j][i] = MAX; 56 } 57 58 } 59 } 60 } 61 62 //利用图的二维数组输出 63 public void printGraph(){ 64 for(int i = 0; i < verticts;i++){ 65 //输出第一行的名称 66 if(i == 0){ 67 System.out.print("* "); 68 for(int x=0;x<verticts;x++){ 69 System.out.print(this.vex[x]+" "); 70 } 71 System.out.println(); 72 System.out.println(" =============================================================="); 73 } 74 //给每行前面输出地址 75 System.out.print(this.vex[i]+" "); 76 for(int j = 0; j < verticts;j++){ 77 System.out.print(this.arc[i][j]+" "); 78 } 79 System.out.println(); 80 System.out.println(); 81 } 82 } 83 84 //深度优先遍历输出 85 public void DFS(Graph G,int i,boolean[] visited){ 86 int j; 87 visited[i] = true; 88 System.out.print(G.vex[i]+" "); //打印顶点的值 89 for(j = 0;j < G.verticts;j++){ 90 if(G.arc[i][j]>0 && !visited[j]){ 91 DFS(G,j,visited); //对访问的邻接顶点递归调用 92 } 93 } 94 95 } 96 public void DFSTraverse(Graph G){ 97 boolean[] visited = new boolean[G.verticts]; 98 for(int i = 0;i < G.verticts;i++){ 99 visited[i] = false; //初始状态所有顶点都是未访问过的状态 100 } 101 for(int i = 0;i < G.verticts;i++){ 102 if(!visited[i]) 103 DFS(G,i,visited); //对未访问过的顶点调用DFS 如果是连通图,则只会执行一次 104 } 105 } 106 107 108 109 //广度优先遍历输出 110 public void BFS(Graph G){ 111 int i, j; 112 Queue<Integer> Q = new LinkedList<Integer>(); 113 boolean[] visited = new boolean[G.verticts]; 114 for(i = 0;i < G.verticts;i++){ 115 visited[i] = false; 116 } 117 118 for(i = 0; i < G.verticts;i++){ //对每一个顶点都进行循环 119 if(!visited[i]){ 120 visited[i] = true; 121 System.out.print("##"+G.vex[i]+" "); 122 Q.offer(i); 123 while(Q != null){ 124 if(Q.peek() != null){ 125 i = Q.poll(); //将队首元素赋值给i 然后出队列 126 }else{return ;} 127 for(j = 0;j < G.verticts;j++){ 128 //判断其他顶点与当前顶点存在边但未被访问过 129 if(G.arc[i][j] > 0 && !visited[j]){ 130 visited[j] = true; 131 System.out.print("##"+G.vex[j]+" "); 132 Q.offer(j); 133 } 134 } 135 } 136 } 137 } 138 } 139 140 //得到最小生成树之普利姆(Prim)算法 141 public void MiniSpanTree_Prim(Graph G){ 142 int min, i, j, k; 143 int [] adjvex = new int[G.verticts]; //保存相关顶点下表 144 int [] lowcost = new int[G.verticts]; //保存相关顶点间边的权值 145 lowcost[0] = 0; //初始化第一个权值为0 ,即vex[0]已经加入到生成树 146 adjvex[0] = 0; //初始化第一个顶点下标为0 147 for(i = 1;i < G.verticts;i++){ 148 lowcost[i] = G.arc[0][i]; //将vex[0]顶点与之有边的权值存入数组 149 adjvex[i] = 0; //初始化都为vex[0]的下标 150 } 151 152 for(i = 1;i < G.verticts;i++){ 153 min = MAX; //初始化最小权值为MAX:65535 154 j = 1; 155 k = 0; 156 //循环所有顶点 157 while(j < G.verticts){ 158 if(lowcost[j] != 0 && lowcost[j] < min){ //如果权值不为0,而且小于最小值 159 min = lowcost[j]; 160 k = j; 161 } 162 j++; 163 } 164 165 System.out.println("("+k+", "+adjvex[k]+")"+" 权长:"+G.arc[adjvex[k]][k]); //打印当前顶点边中权值最小的边 166 lowcost[k] = 0; //将当前顶点的权值设为0,表示此顶点已将完成任务 167 for(j = 1;j < G.verticts;j++){ //循环所有顶点 168 if(lowcost[j] != 0 && G.arc[k][j] < lowcost[j]){ //如果下标为k的顶点各边权值小于此前这些顶点未被加入生成权值 169 lowcost[j] = G.arc[k][j]; //将较小的权值存入lowcost 170 adjvex[j] = k; //将下标为k的顶点存入adjvex 171 } 172 } 173 } 174 } 175 176 177 //得到最小生成树之克鲁斯卡尔(Kruskal)算法 178 //得到边集数组并按权由大到小排序 这一步是利用Edge类来实现的 179 //注意 此函书还有错误,有时候会输出6条边,尚待解决(MinispanTree_kruskal在寻找的过程中不可能形成环路,所以不可能多一条边) 180 //上述错误已经解决,有两个地方出来问题,第一:Edge的begin必须小于end,否则在Find函数中判断将出现错误,因为如果end小于begin的话,end有可能 181 //出现等于0的情况,第二:循环每一条边时,i应该小于G.sides;而我之前写成了i<G.verticts 182 public void MiniSpanTree_Kruskal(Graph G){ 183 Edge edge = new Edge(); 184 edge.Edge_1(G); 185 //edge.PrintEdge(); 186 int i, n, m; 187 int num = 0; //记录找到了多少条边 188 int parent[] = new int[G.verticts]; //定义一个数组用来判断边与边是否形成回路 189 for( i = 0;i < G.verticts;i++){ 190 parent[i] = 0; //初始化数组为-1 191 } 192 for(i = 0;i < G.sides;i++){ // 循环每一条边,15为顶点 193 n = Find(parent,edge.edge[i].begin); 194 m = Find(parent,edge.edge[i].end); 195 if(n != m ){ //n不等于m 说明边与边没有形成环路 196 parent[n] = m;//将此边的尾节点放入下标为起点的parent中 197 System.out.println("("+edge.edge[i].begin+","+edge.edge[i].end+")"+" 权长:"+edge.edge[i].weight); 198 num++; 199 //for(int j = 0;j < G.verticts;j++){ 200 //System.out.print(" !!!!"+parent[j]); //初始化数组为0 201 //} 202 } 203 if(num >= G.verticts) break; //如果找到了(顶点数-1)条边,并且没有构成回路,就已经完成任务了,不用再找了, 204 205 } 206 } 207 public int Find(int[] parent,int f){ //查找连线顶点的尾部下表 208 while(parent[f] > 0){ 209 f = parent[f]; 210 } 211 return f; 212 } 213 214 215 //测试函数 216 public static void main(String[] args){ 217 Graph graph = new Graph(6); //创建一个顶点个数为6的图 218 graph.addGraph(); 219 System.out.println("将图以二维矩阵的方式输出"); 220 graph.printGraph(); 221 System.out.println("深度优先搜索结果"); 222 graph.DFSTraverse(graph); 223 System.out.println(); 224 System.out.print("广度优先搜索结果"); 225 System.out.println(); 226 graph.BFS(graph); 227 System.out.println(); 228 System.out.println("最小生成树之普利姆算法Prim "); 229 graph.MiniSpanTree_Prim(graph); 230 System.out.println(); 231 System.out.println("最小生成树之克鲁斯卡尔算法Kruskal "); 232 graph.MiniSpanTree_Kruskal(graph); 233 234 } 235 } 236 237 238 239 240 //Edge类 利用深度优先遍历得到树的所有路径以及这些路径的权值,并根据权值的大小进行从小到大排序 241 class Edge{ 242 public int begin; //这两个顶点的开始顶点 243 public int end; //这两个顶点的结束顶点 244 public int weight; //两个顶点之间的权值 245 Edge edge[] = new Edge[15]; //edge数组 图的边数没有传入,计算最大值 246 Edge(){} 247 public void Edge_1(Graph G){ 248 DFSTraverse_1(G); //得到edge数组 249 sortEdge(); //对dege进行排序 250 } 251 252 public void SetEdge(int begin,int end,int weight){ 253 this.begin = begin; 254 this.end = end; 255 this.weight = weight; 256 257 } 258 int k=0; //用于数组赋值是计数 259 //利用深度优先遍历得到edge数组 260 public void DFS_1(Graph G,int i,boolean[] visited){ 261 int j; 262 263 visited[i] = true; 264 //System.out.print(G.arc[i][i+1]+" "); //打印顶点的值 265 for(j = 0;j < G.verticts;j++){ 266 if(G.arc[i][j]>0 && !visited[j]){ 267 //System.out.print(G.arc[i][j]+" "); 268 DFS_1(G,j,visited); //对访问的邻接顶点递归调用 269 } 270 if(G.arc[i][j] > 0 && i > j){ 271 this.edge[this.k] = new Edge(); 272 edge[this.k].SetEdge(j,i,G.arc[i][j]); 273 this.k++; 274 } 275 276 } 277 278 } 279 public void DFSTraverse_1(Graph G){ 280 boolean[] visited = new boolean[G.verticts]; 281 for(int i = 0;i < G.verticts;i++){ 282 visited[i] = false; //初始状态所有顶点都是未访问过的状态 283 } 284 for(int i = 0;i < G.verticts;i++){ 285 if(!visited[i]) 286 DFS_1(G,i,visited); //对未访问过的顶点调用DFS 如果是连通图,则只会执行一次 287 } 288 } 289 //对得到的数组进行排序 290 public void sortEdge(){ 291 Edge newEdge = new Edge(); 292 newEdge.edge[0] = new Edge(); 293 for(int i = 0;i < this.edge.length;i++){ 294 for(int j = i;j < this.edge.length;j++){ 295 if(this.edge[i].weight > this.edge[j].weight){ 296 newEdge.edge[0] = this.edge[i]; 297 this.edge[i] = this.edge[j]; 298 this.edge[j] = newEdge.edge[0]; 299 } 300 } 301 } 302 } 303 //输出Edge数组,用以测试Edge是否创建、赋值成功 304 public void PrintEdge(){ 305 for(int i = 0; i < this.edge.length;i++){ 306 System.out.println("数组"+i+": "+this.edge[i].begin+" "+this.edge[i].end+" "+this.edge[i].weight); 307 } 308 } 309 }