1. 图的相关概念
-
一个图(graph)G = (V,E) 由 顶点(vertex) 集 V 和 边(edge) 集 E 组成
-
图可以分为无向图和有向图;也可以分为无权图和有权图
连通图:在无向图中,若任意两个顶点vi与vj都有路径相通,则称该无向图为连通图
强连通图:在有向图中,若任意两个顶点vi与vj都有路径相通,则称该有向图为强连通图
连通网:在连通图中,若图的边具有一定的意义,每一条边都对应着一个数,称为权;权代表着连接连个顶点的代价,称这种连通图叫做连通网
生成树:一个连通图的生成树是指一个连通子图,它含有图中全部n个顶点,但只有足以构成一棵树的n-1条边。一颗有n个顶点的生成树有且仅有n-1条边,如果生成树中再添加一条边,则必定成环
最小生成树:在连通网的所有生成树中,所有边的权值和最小的生成树,称为最小生成树
-
图可以用两种形式来表示,邻接矩阵和邻接表,前者适用于稠密图,后者适用于稀疏图
完全图(complete graph): 是其每一对顶点间都存在一条边的图
稠密图(dense) :图中 E 的条数接近 V*V,也就是接近任意两点之间相连
稀疏图(sparse) :图中 E 的条数远小于 V*V
-
无权图的邻接矩阵和邻接表可以用以下格式来表示
// 有v个顶点
// 邻接矩阵:g[i][j] = 1 表示顶点i和j相连(无权图)
// g[i][j] = 1 表示顶点i和j相连,且权值为1(有权图)
int[][] g = new int[v][v];
// 邻接表:g[i] = [1,2,3] 表示顶点i和顶点1,2,3相连(无权图)
int[][] g = new int[v][v];
// 邻接表:g.get(i) = [1,2,3] 表示顶点i和顶点1,2,3相连(无权图)
List<Set<Integer>> g;
2. 无权图的相关操作
- 无权图的结构和基础操作可如下所示
public class Graph {
// 顶点数
private int n;
// 边数
private int m;
// 是否为有向图
private boolean directed;
// 邻接矩阵
private boolean[][] g;
// 邻接表
private List<Set<Integer>> g2;
public Graph() {
this(10, false);
}
public Graph(int n, boolean directed) {
this.n = n;
this.directed = directed;
this.m = 0;
this.g = new boolean[n][n];
this.g2 = new ArrayList<>();
for (int i = 0; i < n; i++) {
this.g2.add(new HashSet<>());
}
}
private boolean validateVertex(int v) {
return 0 <= v && v < n;
}
public int getVertexNum() {
return this.n;
}
public int getEdgeNum() {
return this.m;
}
// 给两个顶点加一条边
public void addEdge(int v1, int v2) {
if (validateVertex(v1) && validateVertex(v2)) {
if (haveEdge(v1, v2)) {
return;
}
g[v1][v2] = true;
g2.get(v1).add(v2);
if (!directed) {
g[v2][v1] = true;
g2.get(v2).add(v1);
}
m++;
}
}
// 判断两个顶点是否有边直接相连
public boolean haveEdge(int v1, int v2) {
return validateVertex(v1) && validateVertex(v2) && g[v1][v2];
}
// 获取某个顶点的所有邻接顶点
public Set<Integer> getAdjVertex(int v) {
return validateVertex(v) ? g2.get(v) : new HashSet<>();
}
}
2.1 遍历相邻顶点
如果使用领接矩阵,遍历一个顶点的相邻顶点的复杂度是O(v);而使用邻接表的所需要的复杂度是O(1)
2.2 遍历全部顶点
遍历全部顶点可以使用深度优先遍历,遍历的过程中要对顶点做个标记,后续不要再遍历该顶点。这种遍历方式可以求出一个图的连通分量(即这个图有多少个相连通的部分)
public class ConnectedComponent {
private int connectedCount = 0;
/**
* 得到图的连通分量
*
* @param graph 邻接表表示的图
* @return 图的连通分量
*/
public int getConnectedCount(List<Set<Integer>> graph) {
if (graph == null || graph.size() == 0) {
return 0;
}
int size = graph.size();
// 各顶点的连通分量编号
int[] id = new int[size];
boolean[] visit = new boolean[size];
connectedCount = 0;
for (int i = 0; i < size; i++) {
if (!visit[i]) {
dfs(i, visit, id, graph);
connectedCount++;
}
}
return connectedCount;
// 以下结果可以得到两个顶点v1 v2是否相连,相当于并查集的功能
// return id[v1] == id[v2];
}
private void dfs(int v, boolean[] visit, int[] id, List<Set<Integer>> graph) {
visit[v] = true;
id[v] = connectedCount;
for (Integer adj : graph.get(v)) {
if (!visit[adj]) {
dfs(adj, visit, id, graph);
}
}
}
}
2.3 寻路
2.3.1 普通路径
使用深度优先遍历,得到的路径并不是最短路径
public class Path {
public String getPath(List<Set<Integer>> graph, int source, int target) {
if (graph == null || graph.size() == 0) {
return "不存在" + source + "到" + target + "的路径";
}
int size = graph.size();
// 若marked[i] = true,则说明存在从顶点source到顶点i的路径
boolean[] marked = new boolean[size];
// 若from[i] = j,说明i的前一个顶点是j
int[] from = new int[size];
Arrays.fill(from, -1);
getPathDfs(graph, source, marked, from);
if (!marked[target]) {
return "不存在" + source + "到" + target + "的路径";
}
StringBuilder path = new StringBuilder();
int temp = target;
while (temp != -1) {
path.insert(0, temp);
temp = from[temp];
if (temp != -1) {
path.insert(0, " -> ");
}
}
return path.toString();
}
private void getPathDfs(List<Set<Integer>> graph, int v, boolean[] marked, int[] from) {
marked[v] = true;
for (Integer nextV : graph.get(v)) {
if (!marked[nextV]) {
from[nextV] = v;
getPathDfs(graph, nextV, marked, from);
}
}
}
}
2.3.2 无权图最短路径
使用广度优先遍历,可以得到无权图最短路径
public class ShortestPath {
public String getShortestPath(List<Set<Integer>> graph, int source, int target) {
if (graph == null || graph.size() == 0) {
return "不存在" + source + "到" + target + "的路径";
}
int size = graph.size();
// 若marked[i] = true,则说明存在从顶点source到顶点i的路径
boolean[] marked = new boolean[size];
// 若from[i] = j,说明i的前一个顶点是j
int[] from = new int[size];
Arrays.fill(from, -1);
Queue<Integer> queue = new ArrayDeque<>();
queue.offer(source);
marked[source] = true;
// 使用广度优先遍历得到最短路径
while (!queue.isEmpty()) {
Integer curV = queue.poll();
for (Integer nextV : graph.get(curV)) {
if (!marked[nextV]) {
marked[nextV] = true;
from[nextV] = curV;
queue.offer(nextV);
}
}
}
if (!marked[target]) {
return "不存在" + source + "到" + target + "的路径";
}
StringBuilder path = new StringBuilder();
int temp = target;
while (temp != -1) {
path.insert(0, temp);
temp = from[temp];
if (temp != -1) {
path.insert(0, " -> ");
}
}
return path.toString();
}
}