图的定义

图(Graph)是一种复杂的非线性表结构。

  • 顶点(vertex):图中的元素;
  • 边(edge):图中的顶点与其他任意顶点建立连接的关系;
  • 顶点的度(degree):跟顶点相连接的边的条数。
  • 入度(In-degree)和出度(Out-degree):对于有向图,一个顶点的入度是指以其为终点的边数;出度指以该顶点为起点的边数;
  • 图有多种类型,包括有向图、无向图、简单图、多重图、有向图、无向图等;

图的分类

有向图和无向图

图的每条边规定一个方向,那么得到的图称为有向图;相反,边没有方向的图称为无向图。

简单图

  1. 任意两顶点之间只有一条边(在有向图中为两顶点之间每个方向只有一条边);
  2. 每条边所关联的是两个不同的顶点

带权图(weighted graph)

在带权图中,每条边都有一个权重(weight)[非负实数]。

多重图

图中某两个顶点之间的边数多于一条,又允许顶点通过同一条边和自己关联,则称为多重图

图的存储

邻接矩阵(Adjacency Matrix)

邻接矩阵是图的常用存储表示,它的底层依赖一个二维数组。它用两个数组分别存储数据元素(顶点)的信息和数据元素之间的关系(边或弧)的信息。

优点:

  1. 邻接矩阵的存储方式简单、直接,可以高效的获取两个顶点的关系;
  2. 计算方便。(求解最短路径 Floyd-Warshall 算法)

用邻接矩阵来表示一个图,虽然简单、直观,但是比较浪费存储空间。

  1. 对于无向图,a[i][j] == a[j][i],我们只需要存储一个就好,在二维数组中,通过对角线可以划分为两部分,我们只要利用其中一部分的空间就可以了,另外一部分则是多余的。
  2. 存储的是稀疏图(Sparse Matrix):顶点很多,但每个顶点的边并不多,邻接矩阵的存储方法就更加浪费空间了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public class AMWGraph {
private ArrayList vertexList;//存储点的链表
private int[][] edges;//邻接矩阵,用来存储边
private int numOfEdges;//边的数目

public AMWGraph(int n) {
//初始化矩阵,一维数组,和边的数目
edges=new int[n][n];
vertexList=new ArrayList(n);
numOfEdges=0;
}

//得到结点的个数
public int getNumOfVertex() {
return vertexList.size();
}

//得到边的数目
public int getNumOfEdges() {
return numOfEdges;
}

//返回结点i的数据
public Object getValueByIndex(int i) {
return vertexList.get(i);
}

//返回v1,v2的权值
public int getWeight(int v1,int v2) {
return edges[v1][v2];
}

//插入结点
public void insertVertex(Object vertex) {
vertexList.add(vertexList.size(),vertex);
}

//插入结点
public void insertEdge(int v1,int v2,int weight) {
edges[v1][v2]=weight;
numOfEdges++;
}

//删除结点
public void deleteEdge(int v1,int v2) {
edges[v1][v2]=0;
numOfEdges--;
}

//得到第一个邻接结点的下标
public int getFirstNeighbor(int index) {
for(int j=0;j<vertexList.size();j++) {
if (edges[index][j]>0) {
return j;
}
}
return -1;
}

//根据前一个邻接结点的下标来取得下一个邻接结点
public int getNextNeighbor(int v1,int v2) {
for (int j=v2+1;j<vertexList.size();j++) {
if (edges[v1][j]>0) {
return j;
}
}
return -1;
}
}

邻接表(Adjacency List)

邻接表与散列表有点类似,点击了解散列表


图的顶点对应一条链表,链表中存储的是与这个顶点相连接的其他顶点。

邻接表存储起来比较节省空间,但是使用起来就比较耗时间。

当链表过长,为了提高查找效率,我们可以将链表换成其他更加高效的数据结构,如平衡二叉树(红黑树)、跳表、散列表等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Graph { // 无向图
private int v; // 顶点的个数
private LinkedList<Integer> adj[]; // 邻接表

public Graph(int v) {
this.v = v;
adj = new LinkedList[v];
for (int i=0; i<v; ++i) {
adj[i] = new LinkedList<>();
}
}

public void addEdge(int s, int t) { // 无向图一条边存两次
adj[s].add(t);
adj[t].add(s);
}
}

图的遍历

深度优先搜索算法(DFS)

深度优先搜索(Depth-First-Search),类似于树的先序遍历,从图中某个顶点v出发,访问该顶点,然后依次从v的未被访问的邻接点出发继续深度优先遍历图中的其余顶点,直至图中所有与v有路径相通的顶点都被访问完为止。

深度优先搜索的时间复杂度为 O(E),E 表示边的个数;空间复杂度为 O(V),V 表示顶点的个数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//找到顶点后,终止递归的标志
boolean found = false; // 全局变量或者类成员变量

public void dfs(int s, int t) {
found = false;
//visited 记录已经被访问的顶点,避免顶点被重复访问
boolean[] visited = new boolean[v];
//prev 记录搜索路径
int[] prev = new int[v];
for (int i = 0; i < v; ++i) {
prev[i] = -1;
}
recurDfs(s, t, visited, prev);
print(prev, s, t);
}

private void recurDfs(int w, int t, boolean[] visited, int[] prev) {
if (found == true) return;
visited[w] = true;
if (w == t) {
found = true;
return;
}
for (int i = 0; i < adj[w].size(); ++i) {
int q = adj[w].get(i);
if (!visited[q]) {
prev[q] = w;
recurDfs(q, t, visited, prev);
}
}
}

广度优先搜索算法(BFS)

广度优先搜索(Breadth-First-Search),一种“地毯式”层层推进的搜索策略,即先查找离起始顶点最近的,然后是次近的,依次往外搜索。

广度优先搜索的时间复杂度为 O(E),,E 表示边的个数;空间复杂度为 O(V),V 表示顶点的个数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public void bfs(int s, int t) {
if (s == t) return;
//visited 记录已经被访问的顶点,避免顶点被重复访问
boolean[] visited = new boolean[v];
visited[s]=true;
//queue 用来存储已经被访问、但相连的顶点还没有被访问的顶点
Queue<Integer> queue = new LinkedList<>();
queue.add(s);
//prev 记录搜索路径
int[] prev = new int[v];
for (int i = 0; i < v; ++i) {
prev[i] = -1;
}
while (queue.size() != 0) {
int w = queue.poll();
for (int i = 0; i < adj[w].size(); ++i) {
int q = adj[w].get(i);
if (!visited[q]) {
prev[q] = w;
if (q == t) {
print(prev, s, t);
return;
}
visited[q] = true;
queue.add(q);
}
}
}
}

private void print(int[] prev, int s, int t) { // 递归打印 1->9 的路径
if (prev[t] != -1 && t != s) {
print(prev, s, prev[t]);
}
System.out.print(t + " ");
}