图的基本概念java
根据以前博客数据结构整理 中,咱们能够知道node
是一种线性数据结构算法
是一种树结构数组
而这样一种结构就是一种图的结构微信
图的每个点称为顶点(Vertex),一般咱们会给顶点标上序号,而这些序号就能够理解为索引网络
固然这是一种抽象后的概念,在实际中,图能够表示为不少,好比社交网络数据结构
顶点与顶点相连的称为边(Edge)app
而由以上的图中,因为各个顶点相邻的边是没有方向的,因此这种图又被称为无向图(Undirected Graph),在无向图中,只要两个顶点相连,那么不管从哪一个顶点出发均可以到达相邻的顶点。而相似于下图的图是有方向的ide
咱们称之为有向图(Directed Graph),在有向图中,只可以从起始顶点出发到达方向末端的相邻顶点,相反则不能够。因此咱们在考虑现实关系建模的时候,要使用无向图仍是有向图,好比地铁站点之间,不管从哪一个站点出发均可以到达相邻的同一个线路的站点,因此要使用无向图。在社交网络中,若是是微信中,可使用无向图,由于微信中人与人的关系是好友的关系。但有一些社交工具多是一种关注的关系,而不是好友的关系。好比像下图中,Anne关注了Bob,而Bob并无关注Anne,这样咱们就必须使用有向图来进行建模。函数
若是一个图中,顶点与顶点的边只表明一种关系,而没有任何实际的度量,咱们能够称这种图为无权图。而在有一些图中,它们的边表明具备必定的度量信息的意义。咱们称这样的图为有权图。而这个权指的就是这些度量信息。
因此图的分类能够分为四种:
对于图的算法有一些只适合于某一类图,好比最小生成树算法只适用于有权图,拓扑排序算法只适用于有向图,最短路径算法虽然适用于全部类型的图,可是对于无向图和有向图的方式是不同的。
在无向无权图中的概念
若是两个顶点之间有边,咱们称为两点相邻
和一个顶点相邻的全部的边,咱们成为点的邻边
从一个顶点到另外一个顶点所通过的全部边,咱们称为路径(Path),好比下图中从0到6通过了0-1-6,固然从0到6不必定只有这一条路径。
从一个顶点出发,通过其余顶点最终回到起始顶点,咱们称之为环(Loop),好比下图中的0-1-2-3-0就是一个环,固然0-1-6-5-4-3-0也是一个环。
对于单个顶点来讲也能够有一条本身到本身的边,咱们称为自环边,以下图中的0-0。每两个相邻到顶点也可能不仅一条边,咱们能够称为平行边,以下图中的3-4。大多数状况下自环边和平行边没有意义,通常咱们在处理自环边和平行边的时候都是先将其去除,变成没有自环边和平行边的图。固然也有自环边和平行边存在乎义的场景,可是这种状况比较少。在图论中,咱们称没有自环边和平行边的图为简单图。
固然在一个图中,并非全部的顶点都必须是相连的
咱们称在一张图中能够相互链接抵达的顶点的集合为联通份量,因此上面这张图中就有2个联通份量。所以一个图的全部节点不必定所有相连。一个图多是有多个联通份量。
这种有环的图,咱们能够称为有环图。
像这种没法找到从一个顶点出发,通过其余顶点又回到起始顶点的,咱们称为无环图。但它又知足树的定义的,因此树是一种无环图。咱们在图论中谈到树的定义跟在数据结构中说的树不彻底是一个概念,图论中的树的根节点能够是任意节点,而数据结构中说的树每每是固定的一个根节点。虽然树是一种无环图,但一个无环图不必定是树。
由上图可知,它是一个有2个联通份量的图,但能够确定的是1个联通的无环图是树。
由该图咱们能够看到,右边的图跟左边的图的顶点是同样的,区别只在于边,右边的图的边是左边的图的边的子集。咱们将左边的图的一些边删除,就能够获得右边的图。同时右边的图仍是一个树的形状。那么这个过程就能够称为联通图的生成树。因为树必须是联通的,因此只有联通图才有可能生成树。而且该生成树包含原联通图全部的顶点的树。这个树也是保障原联通图能够联通,而且边数最小的那个图,因此该树的边数为:V - 1,这里的V为顶点数。
可是反过来讲,咱们将一个联通图删边,包含全部的顶点,边数为V - 1,却不必定是联通图的生成树。以下面这个图,它就已经再也不联通了,而且产生了环。
那么一个图不必定有生成树,这个图必须是一个联通的图。可是一个图必定有生成深林。一个联通图必定有生成树。对于不止一个联通份量的图,咱们能够将各个联通份量生成树,进而得到生成森林。
在无向图中,一个顶点的度(degree),就是这个顶点相邻的边数,这里也是在说简单图,不考虑自环边和平行边。但在一个有向图中,一个顶点的度的概念不一样。因此咱们能够看到下图中0这个顶点有两个邻边0-一、0-3,因此0这个顶点的度就是2.
接口
public interface Adj { int getV(); int getE(); /** * 是否存在某条边 * @param v * @param w * @return */ boolean hasEdge(int v,int w); /** * 获取和顶点相邻的边 * @param v * @return */ Collection<Integer> adj(int v); /** * 求一个顶点的度(顶点有多少个邻边) * @param v * @return */ int degree(int v); /** * 检测一个顶点的索引是否有效 * @param v */ void validateVertex(int v); }
实现类
/** * 只支持处理简单图 */ public class AdjMatrix implements Adj { //顶点数 private int V; //边数 private int E; //邻接矩阵 private int[][] adj; public AdjMatrix(String filename) { File file = new File(filename); try (Scanner scanner = new Scanner(file)) { V = scanner.nextInt(); if (V < 0) { throw new IllegalArgumentException("V必须为非负数"); } adj = new int[V][V]; E = scanner.nextInt(); if (E < 0) { throw new IllegalArgumentException("E必须为非负数"); } for (int i = 0;i < E;i++) { int a = scanner.nextInt(); validateVertex(a); int b = scanner.nextInt(); validateVertex(b); if (a == b) { throw new IllegalArgumentException("检测到自环边"); } if (adj[a][b] == 1) { throw new IllegalArgumentException("检测到平行边"); } adj[a][b] = 1; adj[b][a] = 1; } }catch (IOException e) { e.printStackTrace(); } } @Override public int getV() { return V; } @Override public int getE() { return E; } @Override public boolean hasEdge(int v, int w) { validateVertex(v); validateVertex(w); return adj[v][w] == 1; } @Override public Collection<Integer> adj(int v) { validateVertex(v); List<Integer> res = new ArrayList<>(); for (int i = 0;i < V;i++) { if (adj[v][i] == 1) { res.add(i); } } return res; } @Override public int degree(int v) { return adj(v).size(); } @Override public void validateVertex(int v) { if (v < 0 || v >= V) { throw new IllegalArgumentException("顶点" + v + "无效"); } } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append(String.format("V = %d,E = %d\n",V,E)); for (int i = 0;i < V;i++) { for (int j = 0;j < V;j++) { builder.append(String.format("%d ",adj[i][j])); } builder.append("\n"); } return builder.toString(); } public static void main(String[] args) { Adj adjMatrix = new AdjMatrix("/Users/admin/Downloads/g.txt"); System.out.println(adjMatrix); } }
g.txt中的内容(第一行表示有7个顶点,9条边;第二行到最后表示哪一个顶点与哪一个顶点相连)
7 9 0 1 0 3 1 2 1 6 2 3 2 5 3 4 4 5 5 6
运行结果
V = 7,E = 9
0 1 0 1 0 0 0
1 0 1 0 0 0 1
0 1 0 1 0 1 0
1 0 1 0 1 0 0
0 0 0 1 0 1 0
0 0 1 0 1 0 1
0 1 0 0 0 1 0
时间复杂度和空间复杂度
空间复杂度 O(V^2)
时间复杂度
建图 O(E)
查看两个节点是否相邻 O(1)
求一个点的相邻节点 O(V)
从邻接矩阵的空间复杂度O(V^2)来看,若是一个图有3000个顶点,若是这个图是一个树的话,那么咱们只存储顶点加边须要存储3000 + (3000 - 1)个信息,就是5999个信息,但若是使用邻接矩阵的话,则须要存储3000^2个信息,即9000000个信息,咱们能够看到这个差距是巨大的。
求一个点的相邻节点的时间复杂度是O(V)的,但其实这个相邻节点的数量就等于该节点的度,而在邻接矩阵中,咱们须要扫描所有3000个顶点才能确认一个顶点的相邻节点,这其实也形成了大量的浪费。若是能找出一个O(degree(v))的算法,那么将比O(V)要小的多。
稀疏图和稠密图
这里稀疏和稠密是指边的多少。若是一个图是一个树的话,那么它确定是一个稀疏图,由于树是全部图里面边最少的图。可是一个有环图并不必定是一个稠密图。假如一个有环无向图有3000个顶点,每一个顶点的度为3,那么这个图有3000 * 3 / 2 = 4500条边。那这个图最多能够有3000 * 2999 / 2 = 4498500条边,它表示每个顶点都跟剩下的2999个顶点相连,因此每个顶点的度为2999。那么4500条边和4498500相比相差了将近1000倍,是一个很大的量级了。
好比说对于上面这个图,咱们看起来可能很稠密,但其实它只是一个稀疏图。由于在该图中度数最大的顶点的度也不过是六、7的样子。虽然这个图的顶点个数大概有几十个,但图中的边数比起它所能容纳的最多的边数,实际上是少不少的。
而上图就是一个典型的稠密图,虽然图中只有21个顶点,要远远少于以前的稀疏图,可是它每个顶点都跟剩余的20个顶点相连,造成的边数很是多。对于这种每个顶点跟剩余全部的顶点相连的图,咱们称为彻底图。在图论中,咱们处理的大多数问题其实都是稀疏图。由于在现实中,咱们对具体的问题进行建模的时候,彻底图或者稠密图是很是少的。可是稀疏图和稠密图之间并无一个黑白分明的界限,没有固定的标准。但咱们能够用一个顶点的度/顶点在彻底图中的度来进行比较,它多是比1/2,1/10,1/100还要少,通常都是稀疏图。
用邻接矩阵来表示一个图的缺点:若是一个图是比较稀疏的话,那么它的空间复杂度会比较高。求一个顶点的相邻顶点所耗费的时间也比较多。而实际生活所处理的图都是稀疏的。因此鉴于邻接矩阵的空间复杂度过大,且相邻节点的时间复杂度较大。咱们就使用邻接表来表示这个图
实现类
public class AdjList implements Adj { //顶点数 private int V; //边数 private int E; //邻接表 private List<Integer>[] adj; @SuppressWarnings("unchecked") public AdjList(String filename) { File file = new File(filename); try (Scanner scanner = new Scanner(file)) { V = scanner.nextInt(); if (V < 0) { throw new IllegalArgumentException("V必须为非负数"); } adj = new LinkedList[V]; for (int i = 0;i < V;i++) { adj[i] = new LinkedList<>(); } E = scanner.nextInt(); if (E < 0) { throw new IllegalArgumentException("E必须为非负数"); } for (int i = 0;i < E;i++) { int a = scanner.nextInt(); validateVertex(a); int b = scanner.nextInt(); validateVertex(b); if (a == b) { throw new IllegalArgumentException("检测到自环边"); } if (adj[a].contains(b)) { throw new IllegalArgumentException("检测到平行边"); } adj[a].add(b); adj[b].add(a); } }catch (IOException e) { e.printStackTrace(); } } @Override public int getV() { return V; } @Override public int getE() { return E; } @Override public boolean hasEdge(int v, int w) { validateVertex(v); validateVertex(w); return adj[v].contains(w); } @Override public Collection<Integer> adj(int v) { validateVertex(v); return adj[v]; } @Override public int degree(int v) { return adj(v).size(); } @Override public void validateVertex(int v) { if (v < 0 || v >= V) { throw new IllegalArgumentException("顶点" + v + "无效"); } } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append(String.format("V = %d,E = %d\n",V,E)); for (int v = 0;v < V;v++) { builder.append(String.format("%d :",v)); adj[v].stream().map(w -> String.format("%d ",w)) .forEach(builder::append); builder.append("\n"); } return builder.toString(); } public static void main(String[] args) { Adj adjList = new AdjList("/Users/admin/Downloads/g.txt"); System.out.println(adjList); } }
运行结果
V = 7,E = 9
0 :1 3
1 :0 2 6
2 :1 3 5
3 :0 2 4
4 :3 5
5 :2 4 6
6 :1 5
空间复杂度 O(V + E)
时间复杂度
建图 O(E * V) 之因此要乘以V是由于检测平行边的时候,须要遍历顶点邻接的全部顶点,若是在一个彻底图下,就要遍历全部的顶点, 而链表是一个线性结构,因此时间复杂度最坏的状况下会有V这么大。这也是一个查重的过程。
查看两点是否相邻 O(degree(v)),最差状况下的彻底图中就是O(V)
求一个点的相邻节点 O(degree(v)), 最差状况下的彻底图中就是O(V)
经过上面的复杂度,咱们能够看到邻接表相比于邻接矩阵,它有两点不足。
将以上的问题提取出来,就是要快速查重和快速查看两点是否相邻
要解决以上的问题,咱们就不能使用链表(LinkedList),咱们可使用哈希表(HashSet,时间复杂度O(1)),或者使用红黑树(TreeSet,时间复杂度O(log V)).
因为红黑树保证了顶点索引的顺序性,咱们使用红黑树来进行转变。而哈希表没法达到该要求,但因为哈希表的时间复杂度更低,若是对顶点没有顺序要求,则使用哈希表更优,在一般的解题过程当中推荐使用哈希表。相比哈希表,红黑树更节省空间。
实现类
public class AdjSet implements Adj { //顶点数 private int V; //边数 private int E; //邻接表 private Set<Integer>[] adj; @SuppressWarnings("unchecked") public AdjSet(String filename) { File file = new File(filename); try (Scanner scanner = new Scanner(file)) { V = scanner.nextInt(); if (V < 0) { throw new IllegalArgumentException("V必须为非负数"); } adj = new TreeSet[V]; for (int i = 0;i < V;i++) { adj[i] = new TreeSet<>(); } E = scanner.nextInt(); if (E < 0) { throw new IllegalArgumentException("E必须为非负数"); } for (int i = 0;i < E;i++) { int a = scanner.nextInt(); validateVertex(a); int b = scanner.nextInt(); validateVertex(b); if (a == b) { throw new IllegalArgumentException("检测到自环边"); } if (adj[a].contains(b)) { throw new IllegalArgumentException("检测到平行边"); } adj[a].add(b); adj[b].add(a); } }catch (IOException e) { e.printStackTrace(); } } @Override public int getV() { return V; } @Override public int getE() { return E; } @Override public boolean hasEdge(int v, int w) { validateVertex(v); validateVertex(w); return adj[v].contains(w); } @Override public Collection<Integer> adj(int v) { validateVertex(v); return adj[v]; } @Override public int degree(int v) { return adj(v).size(); } @Override public void validateVertex(int v) { if (v < 0 || v >= V) { throw new IllegalArgumentException("顶点" + v + "无效"); } } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append(String.format("V = %d,E = %d\n",V,E)); for (int v = 0;v < V;v++) { builder.append(String.format("%d :",v)); adj[v].stream().map(w -> String.format("%d ",w)) .forEach(builder::append); builder.append("\n"); } return builder.toString(); } public static void main(String[] args) { Adj adjset = new AdjSet("/Users/admin/Downloads/g.txt"); System.out.println(adjset); } }
运行结果
V = 7,E = 9
0 :1 3
1 :0 2 6
2 :1 3 5
3 :0 2 4
4 :3 5
5 :2 4 6
6 :1 5
空间复杂度 O(V + E)
时间复杂度
建图 O(E * log V) 相比于链表,红黑树的查重的时间复杂度就要低的多
查看两点是否相邻 O(log V)
求一个点的相邻节点 O(degree(v)), 最差状况下的彻底图中就是O(V)
固然咱们也可使用哈希表来实现
实现类
public class AdjHash implements Adj { //顶点数 private int V; //边数 private int E; //邻接表 private Set<Integer>[] adj; @SuppressWarnings("unchecked") public AdjHash(String filename) { File file = new File(filename); try (Scanner scanner = new Scanner(file)) { V = scanner.nextInt(); if (V < 0) { throw new IllegalArgumentException("V必须为非负数"); } adj = new HashSet[V]; for (int i = 0;i < V;i++) { adj[i] = new HashSet<>(); } E = scanner.nextInt(); if (E < 0) { throw new IllegalArgumentException("E必须为非负数"); } for (int i = 0;i < E;i++) { int a = scanner.nextInt(); validateVertex(a); int b = scanner.nextInt(); validateVertex(b); if (a == b) { throw new IllegalArgumentException("检测到自环边"); } if (adj[a].contains(b)) { throw new IllegalArgumentException("检测到平行边"); } adj[a].add(b); adj[b].add(a); } }catch (IOException e) { e.printStackTrace(); } } @Override public int getV() { return V; } @Override public int getE() { return E; } @Override public boolean hasEdge(int v, int w) { validateVertex(v); validateVertex(w); return adj[v].contains(w); } @Override public Collection<Integer> adj(int v) { validateVertex(v); return adj[v]; } @Override public int degree(int v) { return adj(v).size(); } @Override public void validateVertex(int v) { if (v < 0 || v >= V) { throw new IllegalArgumentException("顶点" + v + "无效"); } } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append(String.format("V = %d,E = %d\n",V,E)); for (int v = 0;v < V;v++) { builder.append(String.format("%d :",v)); adj[v].stream().map(w -> String.format("%d ",w)) .forEach(builder::append); builder.append("\n"); } return builder.toString(); } public static void main(String[] args) { Adj adjhash = new AdjHash("/Users/admin/Downloads/g.txt"); System.out.println(adjhash); } }
运行结果
V = 7,E = 9
0 :1 3
1 :0 2 6
2 :1 3 5
3 :0 2 4
4 :3 5
5 :2 4 6
6 :1 5
空间复杂度 O(V + E)
时间复杂度
建图 O(E)
查看两点是否相邻 O(1)
求一个点的相邻节点 O(degree(v)), 最差状况下的彻底图中就是O(V),但使用哈希表则没法按照邻接顶点的顺序来输出
为了保证邻接顶点的顺序性,后续以使用红黑树为主。
在以前的数据结构整理 中,咱们知道二分搜索树的深度优先遍历为前序遍历,中序遍历和后序遍历。
咱们来看一下前序遍历
private void preOrder(Node node) { if (node == null) { return; } list.add(node.getElement()); //遍历 preOrder(node.getLeft()); //访问全部子树,遍历和node相邻的其余node preOrder(node.getRight()); }
在二分搜索树中,它的节点为一个Node的对象,而在图中的节点为一个顶点的索引值。根据二分搜索树的遍历方式,图的深度优先遍历也是添加节点,再访问跟顶点相邻的其余顶点,这个是没有变的。只不过和图的顶点相邻的可能不仅两个顶点,可能有多个,因此咱们要经过adj()方法获取一个顶点全部相邻的顶点。但跟二分搜索树不一样的是,咱们要判断哪些顶点被访问过,要有一个记录,咱们放在一个数组visited中,若是w这个顶点没有被访问过的话,相应的咱们去递归调用w这个顶点就行了。之因此在二分搜索树中不须要考虑节点有没有被访问过,是由于树中没有环,因此节点的左右子树是必定没有被访问过的,可是在图中由于有环的存在,因此必定要判断这个节点是否被访问过。对于图的深度优先遍历的递归的终止条件就是对于咱们当前的这个v或者一个相邻的顶点都没有,或者它的全部的相邻的节点都已经被遍历过了,就不须要继续递归下去了,递归函数就会直接终止。换句话说,要么G.adj(v)为空,要么.filter(w -> !visited[w])为空,则.forEach(this::dfs)都不会继续执行,递归结束。
private void dfs(int v) { visited[v] = true; list.add(v); G.adj(v).stream().filter(w -> !visited[w]) .forEach(this::dfs); //至关于.forEach(w -> dfs(w)) }
在具体调用上,咱们能够从任意一个顶点出发,好比咱们就从0这个顶点出发。
dfs(0);
咱们用一个全流程图来讲明整个过程
假设有这么一个图,它的邻接表如右边所示,此时咱们创建一个visited数组
visted 0 1 2 3 4 5 6
依然从0开始遍历dfs(0),此时0被遍历过了,咱们找到0的相邻节点一、2.
visted 0 1 2 3 4 5 6
遍历结果: 0
因为1和2都没有被遍历,此时dfs(0) -> dfs(1),咱们再找到1的相邻节点0、三、4
visted 0 1 2 3 4 5 6
遍历结果: 0 1
因为0被遍历过了,此时咱们过滤出来的为三、4,则咱们开始遍历3,dfs(0) -> dfs(1) -> dfs(3)
visted 0 1 2 3 4 5 6
遍历结果: 0 1 3
咱们再找到3的相邻节点一、二、5,因为1被遍历过,此时咱们过滤出来的为二、5,则咱们开始遍历2,dfs(0) -> dfs(1) -> dfs(3) -> dfs(2)
visted 0 1 2 3 4 5 6
遍历结果: 0 1 3 2
咱们再找到2的相邻节点0、三、6,因为0、3都被遍历过,此时咱们过滤出来的为6,则咱们开始遍历6,dfs(0) -> dfs(1) -> dfs(3) -> dfs(2) -> dfs(6)
visted 0 1 2 3 4 5 6
遍历结果: 0 1 3 2 6
咱们再找到6的相邻节点二、5,因为2被遍历过,此时咱们过滤出来的为5,则咱们开始遍历5,dfs(0) -> dfs(1) -> dfs(3) -> dfs(2) -> dfs(6) -> dfs(5)
visted 0 1 2 3 4 5 6
遍历结果: 0 1 3 2 6 5
咱们再找到5的相邻节点三、6,因为三、6都被遍历过,此时咱们过滤出来的结果为空,这次递归结束,返回上层递归dfs(6);但因为6的相邻节点二、5都被遍历过了,返回上层递归dfs(2);2的相邻节点0、三、6都被遍历过了,返回上层递归dfs(3);3的相邻节点一、二、5都被遍历过了,返回上层递归dfs(1);1的相邻节点为0、三、4,其中4没有被遍历过,因此过滤出来的节点为4,则咱们开始遍历4,dfs(0) -> dfs(1) -> dfs(4)
visted 0 1 2 3 4 5 6
遍历结果: 0 1 3 2 6 5 4
咱们再找到4的相邻节点为1,因为1被遍历过了,返回上层递归dfs(1),1的相邻节点为0、三、4都被遍历过了,返回上层递归dfs(0),0的相邻节点一、2都被遍历过了,而0又是递归调用的顶层,因此所有递归结束,所有结果就是[0 1 3 2 6 5 4].
咱们先新建一个h.txt,内容为
7 8 0 1 0 2 1 3 1 4 2 3 2 6 3 5 5 6
接口
public interface DFS { List<Integer> getPre(); List<Integer> getPost(); }
深度优先遍历类
/** * 深度优先遍历 */ public class GraphDFS implements DFS { private Adj G; //访问过的顶点 private boolean[] visited; private List<Integer> pre = new ArrayList<>(); public GraphDFS(Adj G) { this.G = G; visited = new boolean[G.getV()]; dfs(0); } private void dfs(int v) { visited[v] = true; pre.add(v); G.adj(v).stream().filter(w -> !visited[w]) .forEach(this::dfs); } @Override public List<Integer> getPre() { return pre; } @Override public List<Integer> getPost() { return null; } public static void main(String[] args) { Adj g = new AdjSet("/Users/admin/Downloads/h.txt"); DFS graphDFS = new GraphDFS(g); System.out.println(graphDFS.getOrder()); } }
运行结果
[0, 1, 3, 2, 6, 5, 4]
深度优先遍历的改进
如上图所示,当咱们的图有多个联通份量的时候,上面的算法就没法遍历全部的顶点,因此咱们须要对整个深度优先遍历类进行一个改进
/** * 深度优先遍历 */ public class GraphDFS implements DFS { private Adj G; //访问过的顶点 private boolean[] visited; private List<Integer> pre = new ArrayList<>(); public GraphDFS(Adj G) { this.G = G; visited = new boolean[G.getV()]; for (int v = 0;v < G.getV();v++) { if (!visited[v]) { dfs(v); } } } private void dfs(int v) { visited[v] = true; pre.add(v); G.adj(v).stream().filter(w -> !visited[w]) .forEach(this::dfs); } @Override public List<Integer> getPre() { return pre; } @Override public List<Integer> getPost() { return null; } public static void main(String[] args) { Adj g = new AdjSet("/Users/admin/Downloads/h.txt"); DFS graphDFS = new GraphDFS(g); System.out.println(graphDFS.getOrder()); } }
修改一下h.txt的内容,使其变成两个联通份量的图
7 6 0 1 0 2 1 3 1 4 2 3 2 6
运行结果
[0, 1, 3, 2, 6, 4, 5]
在以前的二分搜索树的深度遍历中分红了前序遍历,中序遍历,后序遍历
一、前序遍历
private void preOrder(Node node) { if (node == null) { return; } list.add(node.getElement()); preOrder(node.getLeft()); preOrder(node.getRight()); }
二、中序遍历
private void inOrder(Node node) { if (node == null) { return; } inOrder(node.getLeft()); list.add(node.getElement()); inOrder(node.getRight()); }
三、后序遍历
private void postOrder(Node node) { if (node == null) { return; } postOrder(node.getLeft()); postOrder(node.getRight()); list.add(node.getElement()); }
那么对于二分搜索树的这个概念一样适合于图
private void dfs(int v) { visited[v] = true; list.add(v); G.adj(v).stream().filter(w -> !visited[w]) .forEach(this::dfs); }
在这种在遍历节点前添加元素的,咱们称为深度优先先序遍历
private void dfs(int v) { visited[v] = true; G.adj(v).stream().filter(w -> !visited[w]) .forEach(this::dfs); list.add(v); }
在这种在遍历节点后添加元素的,咱们称为深度优前后序遍历
如今咱们将这两中遍历出来的元素都进行一下存储
/** * 深度优先遍历 */ public class GraphDFS implements DFS{ private Adj G; //访问过的顶点 private boolean[] visited; //深度优先前序遍历结果 private List<Integer> pre = new ArrayList<>(); //深度优前后续遍历结果 private List<Integer> post = new ArrayList<>(); public GraphDFS(Adj G) { this.G = G; visited = new boolean[G.getV()]; for (int v = 0;v < G.getV();v++) { if (!visited[v]) { dfs(v); } } } @Override public List<Integer> getPre() { return pre; } @Override public List<Integer> getPost() { return post; } private void dfs(int v) { visited[v] = true; pre.add(v); G.adj(v).stream().filter(w -> !visited[w]) .forEach(this::dfs); post.add(v); } public static void main(String[] args) { Adj g = new AdjSet("/Users/admin/Downloads/h.txt"); DFS graphDFS = new GraphDFS(g); System.out.println(graphDFS.getPre()); System.out.println(graphDFS.getPost()); } }
运行结果
[0, 1, 3, 2, 6, 4, 5]
[6, 2, 3, 4, 1, 0, 5]
不过因为图的顶点的相邻节点可能不仅2个,因此它并无二叉树的中序遍历。不但图没有,多叉树也没有,仅仅只有二叉树才会有中序遍历。而图的深度优前后序遍历每每在某些条件下会起到很大的做用。
时间复杂度 O(V + E)
咱们来看一下二分搜索树的非递归前序遍历
public void preOrderNR() { Stack<Node> stack = new Stack<>(); stack.push(root); while (!stack.empty()) { Node cur = stack.pop(); list.add(cur.getElement()); if (cur.getRight() != null) { stack.push(cur.getRight()); } if (cur.getLeft() != null) { stack.push(cur.getLeft()); } } }
那么图的非递归深度优先遍历跟二分搜索树同样,只不过,咱们须要对于每个节点,用visited数组判断一下,这个节点是否已经被遍历过了
/** * 深度优先遍历 */ public class GraphDFSNoRecursion implements DFS { private Adj G; //访问过的顶点 private boolean[] visited; //深度优先前序遍历结果 private List<Integer> pre = new ArrayList<>(); public GraphDFSNoRecursion(Adj G) { this.G = G; visited = new boolean[G.getV()]; for (int v = 0;v < G.getV();v++) { if (!visited[v]) { dfs(v); } } } @Override public List<Integer> getPre() { return pre; } @Override public List<Integer> getPost() { return null; } private void dfs(int v) { Stack<Integer> stack = new Stack<>(); stack.push(v); visited[v] = true; while (!stack.empty()) { int cur = stack.pop(); pre.add(cur); G.adj(cur).stream().filter(w -> !visited[w]) .forEach(w -> { stack.push(w); visited[w] = true; }); } } public static void main(String[] args) { Adj g = new AdjSet("/Users/admin/Downloads/h.txt"); DFS graphDFS = new GraphDFSNoRecursion(g); System.out.println(graphDFS.getPre()); } }
运行结果
[0, 2, 6, 3, 1, 4, 5]
在二分搜索树中,非递归的前序遍历跟递归的前序遍历的结果是同样的,由于它是严格根据左右子树的规律来进行入栈和出栈(先右后左),不过在图中,栈的后进先出特性并不能让其与递归的结果顺序保持一致。
至于此,若是不保证结果与递归结果相同的顺序性,固然能够用栈也能够用队列
/** * 深度优先遍历 */ public class GraphDFSQueue implements DFS { private Adj G; //访问过的顶点 private boolean[] visited; //深度优先前序遍历结果 private List<Integer> pre = new ArrayList<>(); public GraphDFSQueue(Adj G) { this.G = G; visited = new boolean[G.getV()]; for (int v = 0;v < G.getV();v++) { if (!visited[v]) { dfs(v); } } } @Override public List<Integer> getPre() { return pre; } @Override public List<Integer> getPost() { return null; } private void dfs(int v) { Queue<Integer> queue = new LinkedList<>(); queue.add(v); visited[v] = true; while (!queue.isEmpty()) { int cur = queue.poll(); pre.add(cur); G.adj(cur).stream().filter(w -> !visited[w]) .forEach(w -> { queue.add(w); visited[w] = true; }); } } public static void main(String[] args) { Adj g = new AdjSet("/Users/admin/Downloads/h.txt"); DFS graphDFS = new GraphDFSQueue(g); System.out.println(graphDFS.getPre()); } }
运行结果
[0, 1, 2, 3, 4, 6, 5]
接口
public interface CC { int getCCCount(); }
实现类
/** * 深度优先遍历 */ public class GraphDFS implements DFS,CC { private Adj G; //访问过的顶点 private boolean[] visited; //深度优先前序遍历结果 private List<Integer> pre = new ArrayList<>(); //深度优前后续遍历结果 private List<Integer> post = new ArrayList<>(); //联通份量个数 private int cccount = 0; public GraphDFS(Adj G) { this.G = G; visited = new boolean[G.getV()]; for (int v = 0;v < G.getV();v++) { if (!visited[v]) { dfs(v); cccount++; } } } @Override public List<Integer> getPre() { return pre; } @Override public List<Integer> getPost() { return post; } @Override public int getCCCount() { return cccount; } private void dfs(int v) { visited[v] = true; pre.add(v); G.adj(v).stream().filter(w -> !visited[w]) .forEach(this::dfs); post.add(v); } public static void main(String[] args) { Adj g = new AdjSet("/Users/admin/Downloads/h.txt"); DFS graphDFS = new GraphDFS(g); System.out.println(graphDFS.getPre()); System.out.println(graphDFS.getPost()); System.out.println(((CC)graphDFS).getCCCount()); } }
运行结果
[0, 1, 3, 2, 6, 4, 5]
[6, 2, 3, 4, 1, 0, 5]
2
接口
public interface CCN extends CC { /** * 检测两个顶点是否联通 * @param v * @param w * @return */ boolean isConnected(int v,int w); /** * 获取各个联通份量各自的顶点 * @return */ List<Integer>[] components(); }
实现类
/** * 检测两个顶点是否在同一个联通份量中 */ public class GraphDFSCC implements CCN { private Adj G; //访问过的顶点 private Integer[] visited; //联通份量个数 private int cccount = 0; public GraphDFSCC(Adj G) { this.G = G; visited = new Integer[G.getV()]; for (int i = 0;i < visited.length;i++) { visited[i] = -1; } for (int v = 0;v < G.getV();v++) { if (visited[v] == -1) { dfs(v,cccount); cccount++; } } } @Override public int getCCCount() { Stream.of(visited).map(v -> v + " ") .forEach(System.out::print); System.out.println(); return cccount; } @Override public boolean isConnected(int v, int w) { G.validateVertex(v); G.validateVertex(w); return visited[v] == visited[w]; } @Override @SuppressWarnings("unchecked") public List<Integer>[] components() { List<Integer>[] res = new ArrayList[cccount]; for (int i = 0;i < cccount;i++) { res[i] = new ArrayList<>(); } for (int v = 0;v < G.getV();v++) { res[visited[v]].add(v); } return res; } private void dfs(int v, int ccid) { visited[v] = ccid; G.adj(v).stream().filter(w -> visited[w] == -1) .forEach(w -> dfs(w,ccid)); } public static void main(String[] args) { Adj g = new AdjSet("/Users/admin/Downloads/h.txt"); CCN graphDFS = new GraphDFSCC(g); System.out.println(graphDFS.getCCCount()); System.out.println(graphDFS.isConnected(1,6)); List<Integer>[] comp = graphDFS.components(); for (int ccid = 0;ccid < comp.length;ccid++) { System.out.print(ccid + " : "); comp[ccid].stream().map(w -> w + " ") .forEach(System.out::print); System.out.println(); } } }
运行结果
0 0 0 0 0 1 0
2
true
0 : 0 1 2 3 4 6
1 : 5
咱们这条路径为可能的一条链接路径,但未必是最短路径。为了便于观察,咱们仍是下面这个2个联通份量到图。
接口
public interface Path { /** * 从源到顶点t是否联通 * @param t * @return */ boolean isConnectedTo(int t); /** * 从源到顶点t所通过的路径 * @param t * @return */ Collection<Integer> path(int t); }
实现类
/** * 单源路径 */ public class SingleSourcePath implements Path { private Adj G; //源 private int source; //访问过的顶点 private boolean[] visited; //路径前节点 private int[] pre; public SingleSourcePath(Adj G,int source) { G.validateVertex(source); this.G = G; this.source = source; visited = new boolean[G.getV()]; pre = new int[G.getV()]; for (int i = 0;i < pre.length;i++) { pre[i] = -1; } //咱们定义源节点的父节点为它本身 dfs(source,source); } @Override public boolean isConnectedTo(int t) { G.validateVertex(t); return visited[t]; } @Override public Collection<Integer> path(int t) { List<Integer> res = new ArrayList<>(); if (!isConnectedTo(t)) { throw new IllegalArgumentException("源顶点" + source + "到目标顶点" + t + "未联通"); } int cur = t; while (cur != source) { res.add(cur); cur = pre[cur]; } res.add(source); Collections.reverse(res); return res; } /** * 深度遍历 * @param v 须要遍历的顶点 * @param parent v的上一个顶点(从哪来的) */ private void dfs(int v,int parent) { visited[v] = true; pre[v] = parent; G.adj(v).stream().filter(w -> !visited[w]) .forEach(w -> dfs(w,v)); } public static void main(String[] args) { Adj g = new AdjSet("/Users/admin/Downloads/h.txt"); Path graphDFS = new SingleSourcePath(g,0); System.out.println("0 -> 6 : " + graphDFS.path(6)); System.out.println("0 -> 5 : " + graphDFS.path(5)); } }
运行结果
0 -> 6 : [0, 1, 3, 2, 6]
Exception in thread "main" java.lang.IllegalArgumentException: 源顶点0到目标顶点5未联通
at com.cgc.cloud.middlestage.user.graph.SingleSourcePath.path(SingleSourcePath.java:43)
at com.cgc.cloud.middlestage.user.graph.SingleSourcePath.main(SingleSourcePath.java:72)
单一源路径的优化
因为咱们在单源路径算法中的深度优先遍历里实际上是遍历了链接的全部顶点,而咱们的目标其实只是为了找出一条从源到目标相连的路径,其实并不必定要遍历全部链接的顶点,只要找到目标顶点即返回,因此咱们作以下的修改。
接口
public interface TPath { /** * 从源到顶点t是否联通 * @return */ boolean isConnected(); /** * 从源到顶点t所通过的路径 * @return */ Collection<Integer> path(); }
实现类
/** * 单源路径 */ public class OncePath implements TPath { private Adj G; //源 private int source; //目标 private int target; //访问过的顶点 private boolean[] visited; //路径前节点 private int[] pre; public OncePath(Adj G, int source,int target) { G.validateVertex(source); G.validateVertex(target); this.G = G; this.source = source; this.target = target; visited = new boolean[G.getV()]; pre = new int[G.getV()]; for (int i = 0;i < pre.length;i++) { pre[i] = -1; } //咱们定义源节点的父节点为它本身 dfs(source,source); for (boolean e : visited) { System.out.print(e + " "); } System.out.println(); } @Override public boolean isConnected() { return visited[target]; } @Override public Collection<Integer> path() { List<Integer> res = new ArrayList<>(); if (!isConnected()) { throw new IllegalArgumentException("源顶点" + source + "到目标顶点" + target + "未联通"); } int cur = target; while (cur != source) { res.add(cur); cur = pre[cur]; } res.add(source); Collections.reverse(res); return res; } /** * 深度遍历 * @param v 须要遍历的顶点 * @param parent v的上一个顶点(从哪来的) */ private boolean dfs(int v,int parent) { visited[v] = true; pre[v] = parent; if (v == target) { return true; } for (int w : G.adj(v)) { if (!visited[w]) { if (dfs(w,v)) { return true; } } } return false; } public static void main(String[] args) { Adj g = new AdjSet("/Users/admin/Downloads/h.txt"); TPath path1 = new OncePath(g,0,3); System.out.println("0 -> 3 : " + path1.path()); TPath path2 = new OncePath(g,0,5); System.out.println("0 -> 5 : " + path2.path()); } }
运行结果
true true false true false false false
0 -> 3 : [0, 1, 3]
true true true true true false true
Exception in thread "main" java.lang.IllegalArgumentException: 源顶点0到目标顶点5未联通
at com.cgc.cloud.middlestage.user.graph.OncePath.path(OncePath.java:50)
at com.cgc.cloud.middlestage.user.graph.OncePath.main(OncePath.java:89)
由结果可知,当咱们遍历到了目标顶点后,其余的顶点将不会去遍历,因此结果第一行只有第1、第2、第四个是true,其余都是false;而若是从源顶点到目标顶点是不联通的,则会遍历完整个联通变量中的节点,因此结果第三行里只有表明5的节点是false,其余都是true.
当咱们要判断一个图中是否有环的时候,不论这个图是否有多个联通份量。咱们在检测环的时候,最主要要看遍历的顶点的相邻节点是不是已经访问过的,且这个访问过的节点不是正在被遍历的这个顶点的父节点。
好比说咱们从0开始遍历到1,咱们看1的相邻节点有0、三、4.虽然0是被遍历过的,可是0是1的父节点,显然它不知足一个环。因而咱们遍历到3.
3的相邻节点为一、2。1虽然被访问过,但而1是3的父节点,因而咱们遍历到了2.
2的相邻节点有0和6,0是被遍历过的,且0不是2的父节点,因此咱们此时能够判定,该图中有环。
环检测类
/** * 无向图的环检测 */ public class CycleDetection { private Adj G; //访问过的顶点 private boolean[] visited; //是否有环 @Getter private boolean hasCycle = false; public CycleDetection(Adj G) { this.G = G; visited = new boolean[G.getV()]; for (int v = 0;v < G.getV();v++) { if (!visited[v]) { if (dfs(v,v)) { hasCycle = true; break; } } } } /** * 从顶点v开始,判断图中是否有环 * @param v * @param parent * @return */ private boolean dfs(int v,int parent) { visited[v] = true; for (int w : G.adj(v)) { if (!visited[w]) { if (dfs(w,v)) { return true; } }else if (w != parent) { return true; } } return false; } public static void main(String[] args) { Adj g = new AdjSet("/Users/admin/Downloads/h.txt"); CycleDetection graphDFS = new CycleDetection(g); System.out.println(graphDFS.isHasCycle()); } }
运行结果
true
二分图的概念
虽然根据上图中,咱们能够很清晰的看出这是一个二分图,可是其实它就是下面这张图
这张图就不是那么一眼看出是一个二分图了。
二分图检测类
/** * 二分图检测 */ public class BipartitionDetection { private Adj G; //访问过的顶点 private boolean[] visited; //各顶点的染色 private int[] colors; //是不是一个二分图 private boolean isBipartite = true; public BipartitionDetection(Adj G) { this.G = G; visited = new boolean[G.getV()]; colors = new int[G.getV()]; for (int i = 0;i < colors.length;i++) { colors[i] = -1; } for (int v = 0;v < G.getV();v++) { if (!visited[v]) { if (!dfs(v,0)) { isBipartite = false; break; } } } } /** * 检测是不是二分图 * @param v * @param color * @return */ private boolean dfs(int v,int color) { visited[v] = true; colors[v] = color; for (int w : G.adj(v)) { if (!visited[w]) { if (!dfs(w,1 - color)) { return false; } }else if (colors[w] == colors[v]) { return false; } } return true; } public boolean isBipartite() { return isBipartite; } public static void main(String[] args) { Adj g = new AdjSet("/Users/admin/Downloads/h.txt"); BipartitionDetection graphDFS = new BipartitionDetection(g); System.out.println(graphDFS.isBipartite()); } }
运行结果
true