Algorithms Fourth Edition
Written By Robert Sedgewick & Kevin Wayne
Translated By 谢路云
Chapter 4 Section 1 无向图html
如下内容修改自
http://www.cnblogs.com/skyivb...
http://www.cnblogs.com/yangec...
http://blog.csdn.net/yafeicha...java
图是若干个顶点(Vertices)和边(Edges)相互链接组成的。边仅由两个顶点链接,而且没有方向的图称为无向图。 在研究图之算法
前,有一些定义须要明确,下图中表示了图的一些基本属性的含义,这里就很少说明。数组
邻接列表数据结构
邻接矩阵 空间V^2app
边的数组 要实现adj(),即要知道一个顶点和哪些顶点相邻,须要遍历每个边函数
对于非稠密的无向图,标准表示是使用邻接表,将无向图的每一个顶点的全部相邻顶点都保存在该顶点对应的元素所指向的一张oop
链表中。全部的顶点保存在一个数组中,使用这个数组就能够快速访问给定顶点的邻接顶点列表。下面就是非稠密无向图的一性能
个例子动画
这种 Graph 的实现的性能有以下特色:
使用的空间和 V + E 成正比
添加一条边所需的时间为常数
遍历顶点 v 的全部相邻顶点所需的时间和 v 的度数成正比(处理每一个相邻顶点所需的时间为常数)
对于这些操做,这样的特性已是最优的了,已经能够知足图处理应用的须要。
public class Graph { private final int V; // number of vertices 顶点数 这个final值在构造函数中初始化后就不能修改了。 private int E; // number of edges 边数 private Bag<Integer>[] adj; // adjacency lists 邻接表数组 将Bag替换成Stack也能够, adj.add()改成adj.push()。 public Graph(int V) { this.V = V; this.E = 0; adj = (Bag<Integer>[]) new Bag[V]; // Create array of lists. for (int v = 0; v < V; v++) // Initialize all lists adj[v] = new Bag<Integer>(); // to empty. //因为 Java 语言固有的缺点,没法建立泛型数组,因此第 10 行中只能建立普通数组后强制转型为泛型数组。这致使在编译时出现警告信息。 //因为 Java 语言固有的缺点,泛型的参数类型不能是原始数据类型,因此泛型的参数类型是 Integer,而不是 int 。这致使了一些性能损失。 } public Graph(In in) { this(in.readInt()); // Read V and construct this graph. int E = in.readInt(); // Read E. for (int i = 0; i < E; i++) { // A an edge. int v = in.readInt(); // Read a vertex, int w = in.readInt(); // read another vertex, addEdge(v, w); // and add edge connecting them. } } public int V() { return V; } public int E() { return E; } public void addEdge(int v, int w) { adj[v].add(w); // Add w to v's list. adj[w].add(v); // Add v to w's list. E++; } //这里为何要返回Iterable?返回Stack<Integer>或者Bag<Integer>能够吗? public Iterable<Integer> adj(int v) { return adj[v]; } public String toString() { StringBuilder s = new StringBuilder(); //待研究 StringBuiler类 String NEWLINE = System.getProperty("line.separator"); s.append(V + " vertices, " + E + " edges" + NEWLINE); for (int v = 0; v < V; v++) { s.append(v + ": "); for (int w : adj[v]) s.append(w + " "); s.append(NEWLINE); } return s.toString(); } }
其余经常使用代码
// 深度 = 相邻顶点的个数/链接边的数量 public static int degree(int v) { int degree = 0; for (int w : G.adj(v)) degree++; return degree; } // 最大深度 public static int maxDegree(Graph G) { int max = 0; for (int v = 0; v < G.V(); v++) if (degree(G, v) > max) max = degree(G, v); return max; } // 平均深度 // 一个顶点的深度为和它相邻的顶点数=链接它的边数 // 平均深度=求和(每一个顶点的边数)/顶点数 = 2E/V public static int avgDegree(Graph G) { return 2 * G.E() / G.V(); } //自环数 public static int numberOfSelfLoops(Graph G) { int count = 0; for (int v = 0; v < G.V(); v++) for (int w : G.adj(v)) if (v == w) count++; return count / 2; // each edge counted twice }
引入符号图是由于,顶点更多的不是数字表示,而是由字符串表示,所以要作一个映射
三种数据结构
符号表 st 键为String(顶点字符串名字), 值为int(索引数字)
数组keys[] 反向索引(经过数字反过来找到字符串)
图对象G
输入数据格式
A B
A D
D E
D B
...
每一行表示一条边,每一行的两个字符串表示链接边的两个顶点。用分隔符(当前为空格,也能够是分号等)分隔。
(Graph的建立能够直接使用符号表,而不使用Bag,使得不须要遍历文件两次。待补充。详情可见An Introduction to Programming in Java: An Interdisciplinary Approach.)
public class SymbolGraph { private ST<String, Integer> st; // String -> index 就是个Map private String[] keys; // index -> String 就是个反向Map private Graph G; // the graph public SymbolGraph(String stream, String sp) { //stream是文件名,sp是分隔符 //下面这一段就是遍历一遍文件,获得全部顶点的字符串名,放进Map里。而后再创建一个数组(反向Map)。这样就创建了字符串和数字的双向映射关系。 st = new ST<String, Integer>(); In in = new In(stream); // First pass while (in.hasNextLine()) // builds the index { String[] a = in.readLine().split(sp); // by reading strings for (int i = 0; i < a.length; i++) // to associate each if (!st.contains(a[i])) // distinct string st.put(a[i], st.size()); // with an index. } keys = new String[st.size()]; // Inverted index for (String name : st.keys()) // to get string keys keys[st.get(name)] = name; // is an array. G = new Graph(st.size()); in = new In(stream); // Second pass while (in.hasNextLine()) // builds the graph { String[] a = in.readLine().split(sp); // by connecting the int v = st.get(a[0]); // first vertex for (int i = 1; i < a.length; i++) // on each line G.addEdge(v, st.get(a[i])); // to all the others. } } public boolean contains(String s) { return st.contains(s); } public int index(String s) { return st.get(s); } public String name(int v) { return keys[v]; } public Graph G() { return G; } }
int s:起点
构造函数:找到与起点连通的其余顶点。在图中从起点开始沿着路径到达其余顶点,并标记每一个路过的顶点。
方法marked(int v):判断s是否和v相连通
方法count(): 有多少个顶点和起点相连?(相似于G.adj(s)的个数)
在谈论深度优先算法以前,咱们能够先看看迷宫探索问题。下面是一个迷宫和图之间的对应关系:
迷宫中的每个交会点表明图中的一个顶点,每一条通道对应一个边。
迷宫探索能够采用Trémaux绳索探索法。即:
在身后放一个绳子
访问到的每个地方放一个绳索标记访问到的交会点和通道
当遇到已经访问过的地方,沿着绳索回退到以前没有访问过的地方:
图示以下:
下面是迷宫探索的一个小动画:
深度优先搜索算法模拟迷宫探索。在实际的图处理算法中,咱们一般将图的表示和图的处理逻辑分开来。因此算法的总体设计
模式以下:
建立一个Graph对象
将Graph对象传给图算法处理对象,如一个Paths对象
而后查询处理后的结果来获取信息
一条路子走到底,不到南门不回头。
从一个顶点v出发,遍历与自身相连通的顶点
在遍历过程当中,设当前被遍历的顶点为w
标记w为已访问
判断w是否已经被访问
是,则继续遍历;
否,则搜索与顶点w相连通的顶点(即调用自身,开始关于顶点w的遍历)
结束遍历
下面是深度优先的基本代码,咱们能够看到,递归调用dfs方法,在调用以前判断该节点是否已经被访问过。
public class DepthFirstSearch{ private boolean[] marked; private int count; public DepthFirstSearch(Graph G, int s) { marked = new boolean[G.V()]; dfs(G, s); } private void dfs(Graph G, int v) { marked[v] = true; count++; for (int w : G.adj(v)) { if (!marked[w]) dfs(G, w); } } public boolean marked(int w) { return marked[w]; } public int count() { return count; } }
上图中是黑色线条表示 深度优先搜索中,全部定点到原点0的路径, 他是经过edgeTo[]这个变量记录的,能够从右边能够看
出,他本质是一颗树,树根便是原点,每一个子节点到树根的路径便是从原点到该子节点的路径。
深度优先搜索标记与起点连通的全部顶点所须要的时间和全部顶点的深度之和成正比。
图是否连通?
从概念上来讲,若是一个图是连通的,那么对于图上面的任意两个节点i, j来讲,它们相互之间能够经过某个路径链接到对方
两个给定顶点是否连通?
连通份量API
实现1.
ConnectedComponents
算法思路
深度优先搜索,依次创建一棵树
其预处理时间和V+E成正比
ConnectedComponents 代码
public class CC { public CC(Graph G) { marked = new boolean[G.V()]; id = new int[G.V()]; for (int s = 0; s < G.V(); s++) //从0开始遍历 if (!marked[s]) { dfs(G, s); count++; //若是dfs返回了,说明全部和0连通的都找完了。就开始找下一个连通的team了,所以count++ } } private void dfs(Graph G, int v) { marked[v] = true; id[v] = count; //新加 保存id for (int w : G.adj(v)) if (!marked[w]) dfs(G, w); } public boolean connected(int v, int w) { return id[v] == id[w]; } public int id(int v) { return id[v]; } public int count() { return count; }
实现2.
UnionFind 详情请见Chapter 1.5
给定一副图G和一个顶点s,从s到给定顶点v是否存在一条路径?若是有,找出这条路径。
路径API
构造函数接收一个顶点s,计算s到与s连通的每一个顶点之间的路径。
如今暂时查找全部路径。
DepthFirstPaths 代码
和DepthFirstSearch几乎一致。
只是添加了路径的记录数组int[] edgeTo
public class DepthFirstPaths { private boolean[] marked; private int[] edgeTo; //新加。第一次访问顶点v的顶点为w。 edgeTo[v]=w private final int s; //新加。把图变成树,构造时的顶点s为树的根结点为s // private int count; 被删去了 public DepthFirstPaths (Graph G, int s) { this.s = s; marked = new boolean[G.V()]; edgeTo = new int[G.V()]; dfs(G, s); } private void dfs(Graph G, int v) { // count++; marked[v] = true; for (int w : G.adj(v)) { if (!marked[w]) { edgeTo[w] = v; //新加,记录路径 dfs(G, w); } } } // 图变成了树,树的根为s // 图被扔掉了,以树的形式保留在数组里了 public boolean hasPathTo(int v) { return marked[v]; } public Iterable<Integer> pathTo(int v) { if (!hasPathTo[v]) return null; Stack<Integer> path = new Stack<Integer>(); for (int w = v; w != s; w = edgeTo[w]) path.push(w); path.push(s); return path; } }
给定的环是无环图吗?
假设没有自环,而且两个顶点间至多只有一条边(即没有平行边)
Cycle 代码
public class Cycle { private boolean[] marked; private boolean hasCycle; public Cycle(Graph G) { marked = new boolean[G.V()]; for (int s = 0; s < G.V(); s++) if (!marked[s]) dfs(G, s, s); } private void dfs(Graph G, int v, int u) { // 新增参数u。这个team手把手带你的师傅被递归进去了。。。看上去很简单,想起来很复杂。。。 marked[v] = true; for (int w : G.adj(v)) if (!marked[w]) dfs(G, w, v); // w是在前线小学徒,v是带你入行的师傅 else if (w != u) hasCycle = true; //在这里判断,这个不等于的判断是由于w是从u过来的,所以要排除掉这个单向通道。 } public boolean hasCycle() { return hasCycle; } }
可以用两种颜色将全部顶点着色,使得任意一条边的两个顶点颜色不一样吗?
这个问题等价于,这是一个二分图吗?(什么叫二分图?)
TwoColor 代码
public class TwoColor { private boolean[] marked; private boolean[] color; //由于只有两色,用boolean就能够了 private boolean isTwoColorable = true; public TwoColor(Graph G) { marked = new boolean[G.V()]; color = new boolean[G.V()]; for (int s = 0; s < G.V(); s++) if (!marked[s]) dfs(G, s); } private void dfs(Graph G, int v) { marked[v] = true; for (int w : G.adj(v)) if (!marked[w]) { color[w] = !color[v]; // 没访问过的赋个颜色 dfs(G, w); } else if (color[w] == color[v]) isTwoColorable = false; //同色的话 就抱歉了 和环的问题有点像 } public boolean isBipartite() { return isTwoColorable; } }
单点最短路径
给定一副图G和一个顶点s,从s到给定顶点v是否存在一条路径?若是有,找出其中最短(所含边数最少)的路径。
五湖四海先识得,泛泛之交后深掘。
先进先出Queue q。
给定顶点s
将v压入q
大循环:弹出q直到q为空,当前顶点为v
小循环:遍历与v相邻的全部顶点,当前顶点为w
判断w是否已访问,若是未被访问
标记w为已访问
压入q
直到大循环q为空,循环结束。
public class BreadthFirstPaths { private final int s; private boolean[] marked; private int[] edgeTo; BreadthFirstPaths (Graph G, int s) { marked = new boolean[G.V()]; edgeTo = new int[G.V()]; this.s = s; bfs(G, s); } public void bfs(Graph G, int s) { Queue<Integer> q = new LinkedList<Integer>(); marked[s] = true; q.offer(s); while (!q.isEmpty()) { int v = q.poll(); for (int w : G.adj(v)) if (!marked[w]) { marked[w] = true; edgeTo[w] = v; q.offer(w); } } } }
待研究,队列Queue q
q.add(); q.remove()会throw异常
q.offer();q.poll()好一些
StringBuiler类Queue q q.offer() q.poll()