四种重要的图模型:java
无向图(简单链接)算法
有向图(链接有方向性)数据库
加权图(链接带有权值)设计模式
加权有向图(链接既有方向性又带有权值)数组
定义:由一组顶点和一组可以将两个顶点相连的边组成。网络
特殊:自环(一条链接一个顶点和其自身的边);平行边(链接同一对顶点的两条边) 数据结构
数学家将含有平行边的图称为多重图;将没有平行边或自环的图称为简单图。现实当中,两点就能够指代一条边。app
两个顶点经过一条边相连,称这两顶点相邻,并称该链接依附于这两个顶点。函数
某个顶点的度数即为依附于它的边的总数。工具
子图是由一幅图的全部边的一个子集(以及它们所依附的全部顶点)组成的图。
路径是由边顺序链接的一系列顶点。
简单路径是一条没有重复顶点的路径。
环是一条至少含有一条边且起点和终点相同的路径。
简单环是一条(除了起点和终点必须相同以外)不含有重复顶点和边的环。
路径或者环的长度为其中所包含的边数。
大多状况研究简单环和简单路径,并会省略简单二字。当容许重复的顶点时,指的都是通常的路径和环。
当两个顶点之间存在一条链接双方的路径时,称一个顶点和另外一个顶点是连通的。
U-V-W-X记为U到X的一条路径;U-V-W-X-U记为U到V到W到X再回到U的一条环。
从任意一个顶点都存在一条路径到达另外一个任意顶点,称这幅图是连通图。
一副非连通的图由若干连通的部分组成,它们都是其极大连通子图。
直观上:若是顶点是念珠,边是链接念珠的线,它们都是物理存在的对象,那么将任意顶点提起来,连通图都将是一个总体,而非连通图则会变成两个或多个部分。
通常来讲:要处理一张图就要一个个地处理它的连通份量(子图)。
无环图:不包含环的图。
树是一副无环连通图。互不相连的树组成的集合称为森林。连通图的生成树是它的一副子图,它含有图中的全部顶点且是一棵树。图的生成树森林是它的全部连通子图的生成树的集合。
树的定义很是通用,稍做改动就能够变成用来描述程序行为的(函数调用层次)模型和数据结构(二叉查找树、2-3树等)。
当且仅当一幅含有V个节点的图G知足下列5个条件之一时,它就是一棵树:
G有V-1条边且不含有环;
G有V-1条边且是连通的;
G是连通的,但删除任意一条边都会使之再也不连通;
G是无环图,但添加任意一条边都会产生一条环;
G中的任意一对顶点之间仅存在一条简单路径。
图的密度是指已经链接的顶点对占全部可能被链接的的顶点对的比例。通常来讲,若是一幅图中不一样的边的数量只占顶点总数V的一小部分,那么就认为这幅图是稀疏的,不然是稠密的。
二分图是一种可以将全部节点分为两部分的图,其中图的每条边所链接的两个顶点都分别属于不一样的部分。二分图会出如今许多场景中。
图的基本操做的API:
两个构造,获得顶点数V( )和边数E( ),增长一条边addEdge( int v, int w )。本节全部算法都基于adj( )方法所抽象的基本操做。第二个构造函数接受的输入由2E+2个整数组成:首先是V, 而后是E, 在而后是 E 对 0到V-1之间的整数,每一个整数对都表示一条边。
要面对的下一个图处理问题就是用哪一种数据结构来表示并实现这份API,这包含两个要求:
必须为可能在应用中碰到的各类类型的图预留出足够的空间;
实例方法的实现必定要快—它们是开发处理图的各类用例的基础。
要求比较模糊,可是仍然能帮咱们在三种图的表示方法中进行选择。
邻接矩阵。用V*V的布尔矩阵,当V和W有边时,定义V行W列元素为TRUE,不然为FALSE。该方法不符合第一个条件,上百万个顶点的图是很常见的.V^2空间不知足。
边的数组。可使用一个Edge类,含有两个int实例变量。表示方法简单可是不知足第二个条件—要实现adj( )须要检查全部边。
邻接表数组。可使用一个以顶点为索引的列表数组,其中每一个元素都是和该顶点相邻的顶点列表。该结构同时知足两个条件。本章一直用它。
除了性能目标,还发现:容许存在平行边至关于排除了邻接矩阵,由于邻接矩阵没法表示它们。
非稠密图的标准表示称为邻接表的数据结构,它将每一个顶点的全部相邻顶点都保存在该顶点对于的元素所指向的一张链表中。使用这个数组就是为了快速访问给定顶点的邻接顶点列表。
使用Bag抽象数据类型(也可用Java中的<LinkedList>)来实现这个链表,这样就能够在常数时间内添加新的边或遍历任意顶点的全部相邻顶点。
这种Graph的实现的性能:
使用的空间和V+E成正比;
添加一条边所须要的时间为常数;
遍历顶点V的全部相邻顶点所须要的时间和V的度数成正比。
对于这样的操做,这样的特性已是最优,能够知足图处理应用的须要,而且支持平行边和自环。边的插入顺序决定了Graph得邻接表中顶点的出现顺序。使用构造函数从标准输入中读入一副图时,就意味着输入的格式和边的顺序决定了Graph的邻接表数组中顶点的出现顺序。
/** * 无向图 */ public class Graph { private int vertexCount; // 顶点数 private int edgeCount; // 边数 private LinkedList<Integer>[] adj; // 邻接表数组 public Graph(int v){ this.adj = new LinkedList[v]; for(int i = 0; i<v; i++) adj[i] = new LinkedList<>();// 初始化邻接表数组 this.vertexCount = v; } public Graph(In in) { this(in.readInt()); int e = in.readInt();//获得边数 // 读取每条边,进行图的初始化操做 for(int i = 0; i<e;i++){ int v = in.readInt(); // 起点 int w = in.readInt(); // 终点 addEdge(v, w); } } /*** 增长一条边*/ public void addEdge(int start, int end) { adj[start].add(end); adj[end].add(start); this.edgeCount++; } public int getEdgeCount() { return edgeCount; } public int getVertexCount() { return vertexCount; } /** 返回顶点v的邻接表*/ public LinkedList<Integer> adj(int v){return adj[v];} /** 把图转化成标准字符串形式*/ public String toString(){ String NEWLINE = System.getProperty("line.separator"); StringBuilder sb = new StringBuilder(); sb.append("vertex count: ").append(getVertexCount()) .append(" edge count: ").append(getEdgeCount()) .append(Config.NEWLINE); for (int v = 0; v < getVertexCount();v++){ LinkedList<Integer> list = adj(v); sb.append(v).append(":\t").append("["); for (int i=0; i < list.size();i++){ sb.append(list.get(i)).append(","); } sb.deleteCharAt(sb.length() - 1); sb.append("]").append(NEWLINE); } return sb.toString(); } public static void main(String[] args) { String dir = Graph.class.getPackage().getName().replace(".", "/"); String path = Graph.class.getClassLoader().getResource(dir+"/tinyG.txt").getPath(); In in = new In(new File(path)); Graph g = new Graph(in); System.out.println(g.toString()); } }
/** * 图的基本经常使用操做工具类 */ public class GraphUtils { /** 计算顶点v的度数*/ public static int degree(Graph graph, int v){return graph.adj(v).size();} /** 计算图中最大的度*/ public static int maxDegree(Graph graph){ int max = 0; for(int i = 0;i<graph.getVertexCount();i++){ int currentDegree = degree(graph, i); max = currentDegree > max ? currentDegree : max; } return max; } /** 计算图的平均度数*/ public static int avgDegree(Graph g){ return 2 * g.getEdgeCount() / g.getVertexCount(); } /** 计算自环的个数*/ public static int numberOfSelfLoops(Graph g){ int count = 0; for(int v = 0; v < g.getVertexCount(); v++) for(int w: g.adj(v)) if(v == w) count++; return count / 2; // 每条边计算了两次 } public static void main(String[] args) { String dir = GraphUtils.class.getPackage().getName().replace(".", "/"); String path = GraphUtils.class.getClassLoader().getResource(dir+"/tinyG.txt").getPath(); In in = new In(new File(path)); Graph g = new Graph(in); for (int i = 0; i < g.getVertexCount(); i++) { System.out.println(i+" degree : "+GraphUtils.degree(g, i)); } System.out.println("the max degree is : " + GraphUtils.maxDegree(g)); System.out.println(g.toString()); System.out.println("avg degree: "+GraphUtils.avgDegree(g)); System.out.println("count of self loop: "+GraphUtils.numberOfSelfLoops(g)); } }
0 degree : 4 1 degree : 1 2 degree : 1 3 degree : 2 4 degree : 3 5 degree : 3 6 degree : 2 7 degree : 1 8 degree : 1 9 degree : 3 10 degree : 1 11 degree : 2 12 degree : 2 the max degree is : 4 vertex count: 13 edge count: 13 0: [5,1,2,6] 1: [0] 2: [0] 3: [4,5] 4: [3,6,5] 5: [0,4,3] 6: [4,0] 7: [8] 8: [7] 9: [12,10,11] 10: [9] 11: [12,9] 12: [9,11] avg degree: 2 count of self loop: 0
将图的表示和实现分离开。为每一个任务建立一个相应的类,用例能够建立相应的对象来完成任务。
探索迷宫方法:tremaux搜索:
选择一条没有标记过的通道,在走过的路上铺一条绳子;
标记全部你第一次路过的路口和通道;
当来到一个标记过的路口时(用绳子)回退到上一个路口;
当回退到得路口已经没有可走的通道时继续回退。
绳子可保证总能找到一条出路,标记则能保证不会两次通过同一条通道或同一个路口。
看Java代码实现:
/** * 图的深度优先搜索算法 */ public class DepthFirstSearch { private int count; private boolean[] marked; // 数组存储每一个顶点是否被遍历过 /** * 从顶点s开始对g进行深搜 * @param g * @param s */ public DepthFirstSearch(Graph g, int s) { marked = new boolean[g.getVertexCount()]; dfs(g, s); } /** 深搜*/ private void dfs(Graph g, int s) { marked[s] = true; // 1.标记顶点s count++; // 2.count数加一 LinkedList<Integer> list = g.adj(s);// 3.获取s的邻接表 for(int w: list) // 4.对邻接表进行遍历 if(!isMarked(w)) dfs(g,w); // 5.若是遍历到的顶点没有被标记过,对该顶点继续递归深搜 } /** 顶点w是否和起点s相连通*/ public boolean isMarked(int w){return marked[w];} /** 与起点s连通的顶点数量*/ public int count(){return count;} public static void main(String[] args) { String dir = DepthFirstSearch.class.getPackage().getName().replace(".", "/"); String path = DepthFirstSearch.class.getClassLoader().getResource(dir+"/tinyG.txt").getPath(); In in = new In(new File(path)); Graph g = new Graph(in); int start = 0; DepthFirstSearch search = new DepthFirstSearch(g, start); System.out.print("start vertex: "+ start+". "); StringBuilder sb = new StringBuilder(); for(int i = 0; i< g.getVertexCount(); i++) if(search.isMarked(i)) sb.append(" "+ i); System.out.println("Connected " + sb.toString()); // 若是和s连通的顶点数量和图的顶点数量相同,说明是连通图 if(search.count() == g.getVertexCount()) System.out.println("g is a connected graph."); else System.out.println("g is not a connected graph."); } }
start vertex: 0. Connected 0 1 2 3 4 5 6 g is not a connected graph.
“两个给定顶点是否连通?”等价于“两个给定的顶点之间是否存在一条路径”,也叫路径检测问题。
union-find算法的数据结构并不能解决找出这样一条路径问题,DFS是已经学习过的方法中第一个可以解决该问题的算法
能解决的另外一问题:单点路径----给定一幅图和一个起点s,“从S到给定的顶点V是否存在一条路径,若是有,找出”
构造函数接受一个起点S做为参数,计算S到与S连通的每一个顶点之间的路径。在为S建立了Paths对象后,用例能够调用pathTo()实例方法来遍历从S到任意和S连通的顶点的路径上的全部顶点。之后会实现只查找具备某些属性的路径。
Java实现
/** * 深搜寻找路径问题 */ public class DepthFirstPaths { private boolean[] marked; private int[] edgeTo; // 路径 private int start; // 起点 public DepthFirstPaths(Graph g, int s){ marked = new boolean[g.getVertexCount()]; edgeTo = new int[g.getVertexCount()]; this.start = s; dfs(g, s); } private void dfs(Graph g, int s) { marked[s] = true; for(int w: g.adj(s)){ if(!marked[w]){ // 若是w没有被标记过,把路径数组中的w处置为s,意思:从s到达了w。此处记录了每一次深搜的路径节点 edgeTo[w] = s; dfs(g, w); } } } /** 从起点s到顶点v是否存在通路*/ public boolean hasPathTo(int v){return marked[v];} public Stack<Integer> pathTo(int v){ if(!hasPathTo(v)) return null; Stack<Integer> stack = new Stack<>(); for(int x = v; x!=start; x=edgeTo[x]) // 从终点开始,倒着找起点,依次push入栈 stack.push(x); stack.push(start);// for循环到起点处终止,因此在循环结束后要把起点入栈,至此 一条完整的路径依次入栈 return stack; } public static void main(String[] args) { String dir = DepthFirstPaths.class.getPackage().getName().replace(".", "/"); String path = DepthFirstPaths.class.getClassLoader().getResource(dir+"/tinyG.txt").getPath(); In in = new In(new File(path)); Graph g = new Graph(in); int start = 0; DepthFirstPaths pathSearch = new DepthFirstPaths(g, start); StringBuilder sb = new StringBuilder(); for(int i = 0; i<g.getVertexCount(); i++){ if(i == start) continue; if(!pathSearch.hasPathTo(i)){ System.out.println(start+" to "+ i +" : not connected."); continue; } sb.setLength(0); sb.append(start).append(" to ").append(i).append(": "); Stack<Integer> p = pathSearch.pathTo(i); while(!p.isEmpty()) sb.append(p.pop()).append("->"); sb.deleteCharAt(sb.length()-1); sb.deleteCharAt(sb.length()-1); System.out.println(sb.toString()); } } }
0 to 1: 0->1 0 to 2: 0->2 0 to 3: 0->5->4->3 0 to 4: 0->5->4 0 to 5: 0->5 0 to 6: 0->5->4->6 0 to 7 : not connected. 0 to 8 : not connected. 0 to 9 : not connected. 0 to 10 : not connected. 0 to 11 : not connected. 0 to 12 : not connected.
深搜获得的路径不只取决于图的结构,还取决于图的表示和递归调用的性质。咱们天然对最短路径感兴趣:
单点最短路径。给定一幅图和一个起点S,从S到给定顶点V是否存在一条路径?若是有,请找出其中最短的那条(所含边数最少)。
DFS遍历图的顺序和找出最短路径的目标无关。
BFS为了这个目标而出现。要找到从S到V得最短路径,从S开始,在全部由一条边就能够到达的顶点中查找V, 若是找不到就继续在与S距离两条边的全部顶点中查找,如此一直执行。
DFS好像是一我的在走迷宫,BFS则像一组人在一块儿朝各个方向走这个迷宫,每一个人都有本身的绳子,当出现新的叉路时,能够假设一个探索者能够分裂为更多的人来搜索。当来个那个探索者相遇的时候,合二为一,并继续使用先到达者的绳子。
在程序中,搜索一幅图时遇到有多条边须要遍历的状况,咱们会选择其中一条并将其余通道留到之后再继续搜索。在DFS中,用了一个能够下压的栈,以支持递归搜索。使用LIFO的规则来描述压栈和走迷宫时先探索相邻的通道相似。从有待搜索的通道中选择最晚遇到过的那条。
在BFS中但愿按照与起点的距离的顺序来遍历全部的顶点:使用FIFO先进先出队列来代替栈LIFO后进先出 便可。将从有待搜索的通道中选择最先遇到的那条。
实现:
算法4.2实现了BFS。使用队列保存全部已经被标记过但其邻接表还未被检查过的顶点。先将起点加入队列,而后重复下面步骤直到队列为空:
取队列中的下一个顶点V并标记它;
将与V相邻的全部未被标记过的顶点加入队列。
算法4.2中的方法不是递归的,不像递归中隐式使用的栈,而是显式地使用了一个队列。
从队列中删除0,将相邻顶点2 1 5加入队列,标记它们并分别将它们在edgeTo[ ]中的值置为0;队列: 0 2 1 5
从队列中删除2,并检查相邻顶点0 1 3 4, 0和1已经被标记,将3和4这两个没被标记的加入队列,标记它们,并分别将它们在edgeTo[ ] 中的值设为2;队列: 0 2 1 5 3 4
删除1,检查相邻点0 2,发现都已经被标记;队列: 0 2 1 5 3 4
删除5, 检查相邻点 0 3, 发现都已经被标记;队列: 0 2 1 5 3 4
删除3, 检查相邻点 2 4 5, 发现都已经被标记;队列: 0 2 1 5 3 4
删除4, 检查相邻点 2 3, 发现都已经被标记;队列: 0 2 1 5 3 4
/** * 广搜找到最短路径 * 对于从s可达的任意顶点v,广搜都能找到一条从s到v的最短路径 * (没有其余从s到v的路径所含边比这条路径更少) * 广搜所需时间在最坏状况下和(v + e)成正比。 */ public class BreadthFirstPaths { private boolean[] marked; private int[] edgeTo; private int start; public BreadthFirstPaths(Graph g, int s){ this.start = s; marked = new boolean[g.getVertexCount()]; edgeTo = new int[g.getVertexCount()]; bfs(g, s); } private void bfs(Graph g, int s) { Queue<Integer> queue = new Queue<>(); marked[s] = true; // 标记起点 queue.enqueue(s); // 起点入队 while(!queue.isEmpty()){ int head = queue.dequeue(); // 从队列中取出队首 LinkedList<Integer> list = g.adj(head); // 获得队首的邻接表 for(int w: list){ //遍历邻接表 if(!marked[w]){ // 若当前节点没有被标记过 edgeTo[w] = head; // 1.存入路径 marked[w] = true; // 2.进行标记 queue.enqueue(w); // 3.节点入队 } } } } /** 从起点s到顶点v是否存在通路*/ public boolean hasPathTo(int v){return marked[v];} /** 返回从起点s到顶点v的一条最短路径*/ public Stack<Integer> pathTo(int v){ if(!hasPathTo(v)) return null; // 若不存在到v的路径,返回Null Stack<Integer> path = new Stack<>(); for(int x = v; x!=start; x=edgeTo[x]) path.push(x); path.push(start); return path; } public static void main(String[] args) { String dir = BreadthFirstPaths.class.getPackage().getName().replace(".", "/"); String path = BreadthFirstPaths.class.getClassLoader().getResource(dir+"/tinyG.txt").getPath(); In in = new In(new File(path)); Graph g = new Graph(in); int start = 5; BreadthFirstPaths bfPath = new BreadthFirstPaths(g, start); for(int i = 0; i<g.getVertexCount();i++){ if(i == start) continue; if(!bfPath.hasPathTo(i)){ System.out.println(start + " to "+ i + " : not connected."); continue; } StringBuilder sb = new StringBuilder(); sb.append(start + " to "+ i + " : "); Stack<Integer> p = bfPath.pathTo(i); while(!p.isEmpty()){ sb.append(p.pop() + "->"); } sb.deleteCharAt(sb.length() - 1); sb.deleteCharAt(sb.length() - 1); System.out.println(sb.toString()); } } }
5 to 0 : 5->0 5 to 1 : 5->0->1 5 to 2 : 5->0->2 5 to 3 : 5->3 5 to 4 : 5->4 5 to 6 : 5->0->6 5 to 7 : not connected. 5 to 8 : not connected. 5 to 9 : not connected. 5 to 10 : not connected. 5 to 11 : not connected. 5 to 12 : not connected.
对于这个例子,edgeTo[]数组在第二步以后就已经完成了。和深搜同样,一点全部顶点都已经被标记,余下的计算工做就只是在检查链接到各个已被标记的顶点的边而已。
命题:对于从S可达到的任意顶点V, 广搜都能找到一条从S到V的最短路径(没有其余从S到V得路径所含的边比这条路径更少)
续: 广搜所需的时间在最坏状况下和V+E成正比。
DFS和BFS都会先将起点存入数据结构中,而后重复如下步骤知道数据结构被清空:
取其中的下一个顶点并标记它;
将V的全部相邻而又未被标记的顶点加入数据结构。
不一样之处在于从数据结构中获取下一个顶点的规则:广搜是最先加入的顶点;深搜是最晚加入的顶点。这种差别获得了处理图的两种彻底不一样的视角,不管哪一种,全部与起点连通的顶点和边都会被检查到。
深搜下一个直接应用就是找出一幅图的全部连通份量。API:
CC的实现使用了marked[ ]数组来寻找一个顶点做为每一个连通份量中深度优先搜索的起点。递归的深搜第一次调用的参数是顶点0,会标记全部与0连通的顶点。而后构造函数中的for循环会查找每一个没有被标记的顶点并递归调用dfs来标记和它相邻的全部顶点。另外,它还使用了一个以顶点做为索引的数组id[ ],将同一个连通份量中的顶点和连通份量的标识符关联起来。这个数组使得connected( )方法的实现变得十分简单。
/** * 强连通份量 */ public class CC { private boolean[] marked; private int[] id; private int count; public CC(Graph g){ marked = new boolean[g.getVertexCount()]; id = new int[g.getVertexCount()]; for(int s = 0; s < g.getVertexCount(); s++){ if(!marked[s]){ dfs(g,s); count++; } } } private void dfs(Graph g, int v) { marked[v] = true; id[v] = count; for(int w: g.adj(v)) if(!marked[w]) dfs(g,w); } /** v和w连通吗*/ public boolean connected(int v, int w) { return id[v] == id[w]; } /** v所在的连通份量的标识符*/ public int id(int v) { return id[v]; } /** 连通份量数*/ public int count() {return count;} public static void main(String[] args) { String dir = CC.class.getPackage().getName().replace(".", "/"); String path = CC.class.getClassLoader().getResource(dir+"/tinyG.txt").getPath(); In in = new In(new File(path)); Graph g = new Graph(in); CC cc = new CC(g); int m = cc.count(); System.out.println("number of components: "+ m); LinkedList<Integer>[] components = new LinkedList[m]; for(int i =0;i<m;i++) components[i] = new LinkedList<>(); for(int v = 0; v< g.getVertexCount(); v++) components[cc.id(v)].add(v); for(int i=0;i<m;i++){ for(int v: components[i]) System.out.print(v + " "); System.out.println(); } } }
number of components: 3 0 1 2 3 4 5 6 7 8 9 10 11 12
其实现基于一个由顶点索引的数组id[ ].若V属于第i个连通份量,则id[v]的值为i。构造函数会找出一个未被标记的顶点并调用递归函数dfs( )来标记并区分出全部和它连通的顶点,如此重复直到全部的顶点都被标记并区分。
命题C:深搜的预处理使用的时间和空间与V+E成正比且能够在常数时间内处理关于图的连通性查询。
和union-find算法对比:理论上深搜比union-find快,由于能保证所需时间是常数,而union-find不行;但在实际中,该差别微不足道。union-find更快,由于它不须要完整的构造并表示一幅图。更重要的是:union-find算法是一种动态算法(在任什么时候候都能用接近常数的时间检查两个顶点是否连通,甚至是在添加一条边的时候),但深搜就必须对图进行预处理。
所以,在完成只须要判断连通性或是须要完成有大量连通性查询和插入操做混合等相似的任务时,更倾向使用union-find,而深搜更适合实现图的抽象数据类型,由于可以更有效的利用已有数据结构。
DFS已经解决了几个基础问题。该方法很简单,递归实现使得咱们可以进行复杂的运算并为一些图的处理问题给出简洁的解决方法。
下面对两个问题进行解答:
检测环:给定的图是无环图吗?
双色问题:可以用两种颜色将图的全部顶点着色,使得任意一条边上的两个端点的颜色都不一样吗?这个问题等价于:这是一幅二分图吗?
/** * 给定的图是无环图吗 * 检测自环:假设没有自环,没有平行边 */ public class Cycle { private boolean[] marked; private boolean hasCycle; public Cycle(Graph g){ marked = new boolean[g.getVertexCount()]; for(int i = 0;i<g.getVertexCount();i++) if(!marked[i]) dfs(g, i, i); } private void dfs(Graph g, int v, int u) { marked[v] = true; for(int w: g.adj(v)) if(!marked[w]) dfs(g, w, v); // 若w没被标记过,那么从w继续递归深搜,把w的父节点做为第二参数 else if(w != u) hasCycle = true; // 若w被标记过,那么若无环,w必然和父节点相同,不然就是有环 } /** 是否含有环*/ public boolean hasCycle(){return hasCycle;} public static void main(String[] args) { String dir = Cycle.class.getPackage().getName().replace(".", "/"); String pathCycle = Cycle.class.getClassLoader().getResource(dir+"/tinyG.txt").getPath(); String pathNoCycle = Cycle.class.getClassLoader().getResource(dir+"/cycle_test.txt").getPath(); In in = new In(new File(pathCycle)); Graph g = new Graph(in); Cycle c = new Cycle(g); System.out.println(c.hasCycle()); In in2 = new In(new File(pathNoCycle)); Graph g2 = new Graph(in2); Cycle c2 = new Cycle(g2); System.out.println(c2.hasCycle()); } }
true false
/** * 双色问题:可以用两种颜色将图的全部顶点着色,使得任意一条边上的两个端点的颜色都不一样吗? * 等价于:判断是不是二分图的问题 */ public class TwoColor { private boolean[] marked; private boolean[] color; private boolean isColorable; public TwoColor(Graph g){ isColorable = true; marked = new boolean[g.getVertexCount()]; color = new boolean[g.getVertexCount()]; for(int i = 0; i<g.getVertexCount(); i++)//遍历全部顶点 if(!marked[i]) dfs(g, i);//没有mark就进行深搜 } private void dfs(Graph g, int v) { marked[v] = true; // 标记 for(int w: g.adj(v)) // 对邻接表进行遍历 if(!marked[w]){ // 若是没有被标记 color[w] = !color[v]; // 当前w节点颜色置为和父节点不一样的颜色 dfs(g, w); // 对当前节点继续深搜 }else if(color[w] == color[v]){ // 若是已经被标记,看是否颜色和父节点相同 isColorable = false; // 若相同则不是二分图 } } /** 是不是二分图*/ public boolean isBipartite(){return isColorable;} public static void main(String[] args) { String dir = TwoColor.class.getPackage().getName().replace(".", "/"); String path = TwoColor.class.getClassLoader().getResource(dir+"/color_test.txt").getPath(); String path2 = TwoColor.class.getClassLoader().getResource(dir+"/color_test2.txt").getPath(); In in = new In(new File(path)); Graph g = new Graph(in); TwoColor t = new TwoColor(g); System.out.println(t.isBipartite()); In in2 = new In(new File(path2)); Graph g2 = new Graph(in2); TwoColor t2 = new TwoColor(g2); System.out.println(t2.isBipartite()); } }
true false
典型应用中,图都是经过文件或者网页定义的,使用的是字符串而非整数来表示和指代顶点。为了适应这样的应用,定义拥有如下性质的输入格式:
顶点名为字符串
用指定的分隔符来隔开顶点名(容许顶点名中含有空格)
每一行都表示一组边的集合,每条边都链接着这一行的第一个名称表示的顶点和其余名称所表示的顶点
顶点总数V和边的总数E都是隐式定义的。
例子:
API
定义了一个构造来读取并构造图,用name( )方法和index( )方法将输入流中的顶点名和图算法使用的顶点索引对应起来。
例子:飞机场routes.txt--输入机场代码查找从该机场起飞到达的城市,但这些信息并非直接从文件中能获得的。
例子:电影movies.txt--输入一部电影名字获得演员列表。这不过是在照搬文件中对应的行数据,
但输入演员名字 查看其出演的电影列表,至关于反向索引。
尽管数据库的构造是为了将电影名链接到演员,二分图模型同时也意味着将演员链接到电影名。
二分图的性质自动完成了反向索引。这将成为处理更复杂的和图有关的问题的基础。
SymbolGraph用到了3种数据结构:
一个符号表st,键的类型为String(顶点名),值得类型为int(索引);
一个数组keys[ ],用做反向索引,保存每一个顶点索引对应的顶点名;
一个Graph对象G,使用索引来引用图中的顶点。
SymbolGraph会遍历两遍数据结构来构造以上数据结构,主要是由于构造Graph对象须要顶点总数V。在典型的实际应用中,在定义图的文件中指明V和E可能会不方便,从而有了SymbolGraph,这样就能够方便地在routes.txt或者movies.txt中添加或者删除条目而不用但系须要维护边或者顶点的总数。
Java实现
/** * 符号图 */ public class SymbolGraph { private HashMap<String, Integer> map; // key:顶点名 value:索引 private String[] keys; // 反向索引,保存每一个顶点索引对应的顶点名 private Graph g; // 使用索引来引用图中的顶点 public SymbolGraph(String path, String sp){ map = new HashMap<>(); BufferedReader reader; String line; try { reader = new BufferedReader(new FileReader(new File(path))); while((line = reader.readLine()) != null){//第一遍,构造索引 String [] vertexs = line.split(sp); for(String s : vertexs) if(!map.containsKey(s)) map.put(s, map.size()); } reader.close(); keys = new String[map.size()]; for(String name: map.keySet()){ // 遍历map的key,构造顶点名的反向索引 keys[map.get(name)] = name; } g = new Graph(map.size()); line = ""; reader = new BufferedReader(new FileReader(new File(path))); while((line = reader.readLine()) != null){ // 第二遍,构造图,将每一行的顶点和该行其余点相连 String[] strs = line.split(sp); int start = map.get(strs[0]);//获取起点 for(int i = 1; i< strs.length; i++) g.addEdge(start, map.get(strs[i])); } reader.close(); } catch (IOException e) { e.printStackTrace(); } } /** key是一个顶点吗*/ public boolean contains(String key){return map.containsKey(key);} /** key的索引*/ public int index(String key){return map.get(key);} /** 索引v的顶点名*/ public String name(int v){return keys[v];} /** 隐藏的Graph对象*/ public Graph graph(){return g;} public static void main(String[] args) { String dir = Cycle.class.getPackage().getName().replace(".", "/"); String path = Cycle.class.getClassLoader().getResource(dir+"/routes.txt").getPath(); SymbolGraph sg = new SymbolGraph(path, " "); Graph g = sg.graph(); HashMap<String, Integer> map = sg.map; for(Entry<String, Integer> s: map.entrySet()){ System.out.println(s.getKey() + "-" +s.getValue()); } System.out.println(g.toString()); String start = "JFK"; if(!sg.contains(start)){ System.out.println("起点"+start + " 不在数据库."); return; } int s = sg.index(start); BreadthFirstPaths bfs = new BreadthFirstPaths(g, s); String end = "LAS"; if(!sg.contains(end)){ System.out.println("终点"+end + " 不在数据库."); }else{ int t = sg.index(end); if(!bfs.hasPathTo(t)){ System.out.println(start +" 和 " + end + " 没有路径相同."); return; } Stack<Integer> stack = bfs.pathTo(t); StringBuilder sb = new StringBuilder(); while(!stack.isEmpty()){ sb.append(sg.name(stack.pop())).append(" "); } System.out.println("起点"+start+"到终点"+end+"的路径为:"); System.out.println(sb.toString()); } } }
LAS-9 LAX-8 DFW-5 ORD-2 JFK-0 HOU-4 ATL-7 DEN-3 PHX-6 MCO-1 vertex count: 10 edge count: 18 0: [1,7,2] 1: [0,7,4] 2: [3,4,5,6,0,7] 3: [2,6,9] 4: [2,7,5,1] 5: [6,2,4] 6: [5,2,3,8,9] 7: [0,4,2,1] 8: [6,9] 9: [3,8,6] 起点JFK到终点LAS的路径为: JFK ORD DEN LAS
一样能够把电影-演员做为例子输入:
这个Graph实现容许用例用字符串代替数字索引来表示图中的顶点。
它维护了
实例变量st(符号表用来映射顶点名和索引)
keys(数组用来映射索引和顶点名)
g(使用索引表示顶点的图)
为了构造这些数据结构,代码会将图的定义处理两遍(定义的每一行都包含一个顶点以及它的相邻顶点列表,用分隔符sp隔开)
图处理的一个经典问题就是,找到一个社交网络之中两我的间隔的度数。
演员K演过不少电影,为图中每一个演员附一个K数:
K本人为0,
全部和K演过同一部电影的人的值为1,
全部(除K外)和K数为1的演员出演过同一部电影的人的值为2,
以此类推。
能够看到K数必须为最短电影链的长度,所以不用计算机,很难知道。
用例DegreesOfSeparation所示,BreadthFirstPaths才是咱们所须要的程序,经过最短路径来找出movies.txt中任意演员的K数。
几个基本概念:
图的术语;
一种图的表示方法,可以处理大型而稀疏的图;
和图处理相关的类的设计模式,其实现算法经过在相关的类的构造函数中对图进行预处理,构造所需的数据结构来高效支持用例对图的查询;
DFS&BFS
支持使用符号做为图的顶点名的类。
上表总结了本节全部图算法的实现。适合做为图处理的入门学习。随后学习复杂类型图以及更加困难的问题时,会用到这些代码的变种。
考虑了边的方向以及权重以后,一样地问题会变得困可贵多,但一样地算法仍然凑效并将成为解决更复杂问题的起点。