Algorithms Fourth Edition
Written By Robert Sedgewick & Kevin WayneTranslated By 谢路云Chapter 4 Section 1 无向图以下内容修改自
无向图的建立
无向图的定义
图是若干个顶点(Vertices)和边(Edges)相互连接组成的。边仅由两个顶点连接,并且没有方向的图称为无向图。 在研究图之
前,有一些定义需要明确,下图中表示了图的一些基本属性的含义,这里就不多说明。
无向图API
数据结构
邻接列表
邻接矩阵 空间V^2
边的数组 要实现adj(),即要知道一个顶点和哪些顶点相邻,需要遍历每一个边
对于非稠密的无向图,标准表示是使用邻接表,将无向图的每个顶点的所有相邻顶点都保存在该顶点对应的元素所指向的一张
链表中。所有的顶点保存在一个数组中,使用这个数组就可以快速访问给定顶点的邻接顶点列表。下面就是非稠密无向图的一
个例子
这种 Graph 的实现的性能有如下特点:
使用的空间和 V + E 成正比
添加一条边所需的时间为常数
遍历顶点 v 的所有相邻顶点所需的时间和 v 的度数成正比(处理每个相邻顶点所需的时间为常数)
对于这些操作,这样的特性已经是最优的了,已经可以满足图处理应用的需要。
Graph 代码
public class Graph{ private final int V; // number of vertices 顶点数 这个final值在构造函数中初始化后就不能修改了。 private int E; // number of edges 边数 private Bag[] adj; // adjacency lists 邻接表数组 将Bag替换成Stack也可以, adj.add()改为adj.push()。 public Graph(int V) { this.V = V; this.E = 0; adj = (Bag []) new Bag[V]; // Create array of lists. for (int v = 0; v < V; v++) // Initialize all lists adj[v] = new Bag (); // 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 或者Bag 可以吗? public Iterable 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 }
复杂度
符号图
引入符号图是因为,顶点更多的不是数字表示,而是由字符串表示,因此要做一个映射
符号图API
实现
三种数据结构
符号表 st 键为String(顶点字符串名字), 值为int(索引数字)
数组keys[] 反向索引(通过数字反过来找到字符串)
图对象G
SymbolGraph 代码
输入数据格式
A BA DD ED B...每一行表示一条边,每一行的两个字符串表示连接边的两个顶点。用分隔符(当前为空格,也可以是分号等)分隔。(Graph的创建可以直接使用符号表,而不使用Bag,使得不需要遍历文件两次。待补充。详情可见An Introduction to Programming in Java: An Interdisciplinary Approach.)public class SymbolGraph { private STst; // 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 (); 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; }}
深度优先算法
最简搜索API
int s:起点
构造函数:找到与起点连通的其他顶点。在图中从起点开始沿着路径到达其他顶点,并标记每个路过的顶点。方法marked(int v):判断s是否和v相连通方法count(): 有多少个顶点和起点相连?(类似于G.adj(s)的个数)引入:迷宫探索
在谈论深度优先算法之前,我们可以先看看迷宫探索问题。下面是一个迷宫和图之间的对应关系:
迷宫中的每一个交会点代表图中的一个顶点,每一条通道对应一个边。
迷宫探索可以采用Trémaux绳索探索法。即:
在身后放一个绳子
访问到的每一个地方放一个绳索标记访问到的交会点和通道
当遇到已经访问过的地方,沿着绳索回退到之前没有访问过的地方:
图示如下:
下面是迷宫探索的一个小动画:
深度优先搜索算法模拟迷宫探索。在实际的图处理算法中,我们通常将图的表示和图的处理逻辑分开来。所以算法的整体设计
模式如下:
创建一个Graph对象
将Graph对象传给图算法处理对象,如一个Paths对象
然后查询处理后的结果来获取信息
算法思路
一条路子走到底,不到南门不回头。
从一个顶点v出发,遍历与自身相连通的顶点
在遍历过程中,设当前被遍历的顶点为w
标记w为已访问
-
判断w是否已经被访问
是,则继续遍历;
否,则搜索与顶点w相连通的顶点(即调用自身,开始关于顶点w的遍历)
结束遍历
DepthFirstSearch 代码
下面是深度优先的基本代码,我们可以看到,递归调用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; }}
DFS简单例子的追踪
上图中是黑色线条表示 深度优先搜索中,所有定点到原点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[] edgeTopublic 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 IterablepathTo(int v) { if (!hasPathTo[v]) return null; Stack path = new Stack (); 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为空,循环结束。
BreadthFirstPaths 代码
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) { Queueq = new LinkedList (); 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()