嚎羸的博客

因为Hexo是静态博客,部署多有不便,建议查看我的语雀文档

0%

13、图

为什么要有图

为什么要有图

前面我们看了线性表和树

我们知道线性表局限于只有一个前驱和后继的关系

树也只能有一个直接前趋,但是可以有多个后继

但是这里有一些情况,假如我们需要使用多个直接前趋该怎么办呢?比如我们描述多对多关系的时候

假如说现在有学生和老师这种情况,学生要有多个老师,老师也有多个学生

当我们描述这种多对多关系,就需要图

图的基本概述

图的基本概述

图是一种数据结构,其中一个节点可以有零个或者多个相邻的元素

两个节点之间的连接称为边

节点也可以称为顶点

image-20210125120802636

我们看上面的图,这个就是图,其中两个顶点的相连为边,比如BD

图的常用概念

1、顶点:也就是节点,上图的A、B、C、D、E

2、边:两个节点的链接,比如AB

3、路径:和无向图还是有向图有关,比如上图从D到C的路径可以为:D->B->C,或者D->A->B->D

4、无向图:上图就是无向图,也就是顶点的连接没有方向,比如A和B,可以从A->B,也可以从B->A

5、有向图:顶点之间有方向

比如这里

image-20210125121356423

6、带权图:边带有权值,带权图也叫做网

image-20210125121439603

图的表示方式

1、二维数组表示,我们称为邻接矩阵

2、链表表示(或者数组+链表),我们称为邻接表

邻接矩阵

邻接矩阵是表示图形中顶点之间相邻关系的矩阵,对于n个顶点的图来说,矩阵的row和col表示的是1…n个点

image-20210125121722993

在上图中,0代表不直接联通,1代表可直接联通

比如0和0的值就是0,代表不联通,0和1之间是1,代表可联通

但是有一些虽然不可以直接联通,但是是可以通过其他节点间接连通的

邻接表

邻接矩阵其实有个缺点,它为所有的顶点都分配了n个边的空间,其实这里面有很多空间都浪费掉了,所以为了避免资源的浪费,我们推出了邻接表,邻接表只关心存在的边,不关心不存在的边,所以不存在空间的浪费

其实稀疏数组是可以对邻接矩阵进行优化的,但是转来转去太麻烦了,直接看邻接表

image-20210125122345332

在上图中,是使用数组+链表构成的,其中在数组索引中,0代表标号为0的顶点,1代表标号为1的顶点,….

我们看标号为0的链表,举个例子。标号为0的链表上有1、2、3、4,这就代表顶点0可以和1、2、3、4直接相连

再举个例子,看标号为1的链表,它有0、4,代表顶点1可以和顶点0、4直接相连

注意,链表的节点不代表顺序,只代表和谁相连接


图创建和代码实现

思路分析

image-20210126103017870

代码实现以上结构:

1、首先要存储这几个顶点,比如我们使用一个ArrayList来存储

2、图的关系我们使用二维数组来保存这个矩阵int[][] edges,1代表能够直连,0代表不能直连

代码实现

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
package com.howling;

import java.util.ArrayList;
import java.util.Arrays;

public class GraphDemo {
// 图中顶点的集合
private ArrayList<String> vertexList;

// 图中的顶点之间的连接关系,二维矩阵
private int[][] edges;

// 边的条数
private int numOfEdges;

/**
* 构造器
*
* @param n 代表图中顶点的个数
*/
public GraphDemo(int n) {
// 初始化二维矩阵
edges = new int[n][n];

// 初始化顶点的ArrayList
vertexList = new ArrayList<>(n);

// 因为还不知道边的关系,所以初始化为0
numOfEdges = 0;
}

/**
* 插入节点
*
* @param vertex 向集合中插入节点
*/
public void insertVertex(String vertex) {
vertexList.add(vertex);
}

/**
* 获取图中的顶点个数
*
* @return
*/
public int getNumOfVertex() {
return this.vertexList.size();
}

/**
* 根据索引下标获取对应的值,索引取决于顶点添加的顺序
*
* @param index 对应的索引下标
* @return
*/
public String getValueByIndex(int index) {
return vertexList.get(index);
}

/**
* 添加两个顶点之间的边和边的权值
* 假如两个顶点之间可以直接链接就是1,不能直接链接就是0
*
* @param v1 顶点1
* @param v2 顶点2
* @param weight 边的权值
*/
public void insertEdge(int v1, int v2, int weight) {

edges[v1][v2] = weight;
edges[v2][v1] = weight;
numOfEdges++;
}

/**
* 获取边的个数
*
* @return 边的个数
*/
public int getNumOfEdges() {
return this.numOfEdges;
}

/**
* 获取两个顶点之间,边的权值
*
* @param v1 顶点1
* @param v2 顶点2
* @return 权值
*/
public int getWeight(int v1, int v2) {
return edges[v1][v2];
}

public void showGraph() {
Arrays.stream(edges).forEach(e -> System.out.println(Arrays.toString(e)));
}

public static void main(String[] args) {
int n = 5;
String[] VertexValue = {"A", "B", "C", "D", "E"};

GraphDemo graph = new GraphDemo(n);

// 添加顶点
Arrays.stream(VertexValue).forEach(graph::insertVertex);

// 添加边,A-B、A-C、B-C、B-D、B-E
graph.insertEdge(0, 1, 1);
graph.insertEdge(0, 2, 1);
graph.insertEdge(1, 2, 1);
graph.insertEdge(1, 3, 1);
graph.insertEdge(1, 4, 1);

graph.showGraph();
}

}

图的遍历

图的遍历介绍

图的遍历,也就是对节点的访问。一个图中有许多节点,那么我们遍历这种节点需要一些策略

一般来讲,有两种遍历策略:

1、深度优先遍历

2、广度优先遍历

图的深度优先

图的深度优先思想(Depth First Search)

1、深度优先遍历,也就是从初始访问节点出发,初始访问节点可能有多个邻接节点

深度优先遍历的策略是首先访问第一个节点,再以这个被访问的邻接节点作为初始访问节点,继续下一次访问

2、我们可以看到,这种访问策略是优先向纵深方向,而不是对一个节点的所有邻接节点进行的横向访问

3、显然,深度优先是一个递归的过程

深度优先的算法步骤

1
2
3
4
5
6
1、访问初始节点v,并标记v节点已访问
2、查找v的第一个邻接节点w
2.1、假如w不存在(不直接连通),那么返回第一步,从v的下一个节点继续
2.2、假如w存在(直接连通)
2.2.1、假如w未被访问,则标记w已访问,并以w作为初始节点,进行第一步
2.2.2、假如w已经被访问,则查找节点v的其他邻接节点,转向第二步

深度优先举例

image-20210126115438890

我们以上图来举例子,假如我现在从顶点A开始进行深度优先遍历

那么我首先规定,这几个节点的相邻顺序为:A->B->C->D->E

1、A为开始节点,标记为已遍历

2、A找到相邻节点B,发现与B不能直接连通,继续寻找下一个节点

3、A找到相邻节点C,发现可以与C直接连通,则进入C并标记C为已遍历

4、C找到相邻节点A,发现可以连通但是已遍历,继续寻找下一个节点

5、C找到相邻节点B,发现可以连通并且没有遍历,则进入B并标记B为已遍历

6、B寻找到A和C,发现A不可连通,C已经遍历,继续寻找下一个节点

7、B寻找D,发现D满足条件,则进入D并标记D为已遍历

8、D分别找到了A、B、C、E,发现均不满足条件,则返回上一层B

9、B继续寻找到E,发现E满足条件,进入到E并标记E为已遍历

10、E寻找到了A、B、C、D,发现均不满足条件,有没有其他节点,所以推出到上一层B

11、B发现没有其他节点,推出到上一层C

12、C继续寻找D、E,发现均不满足条件,没有了其他节点,退出到A

13、A继续寻找D、E,发现均不满足条件,没有其他节点,结束递归

图的深度优先代码实现

image-20210126115438890

我们按照这张图来进行一次深度优先

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
package com.howling;

import java.util.ArrayList;
import java.util.Arrays;

public class GraphDemo {
// 图中顶点的集合
private ArrayList<String> vertexList;

// 图中的顶点之间的连接关系,二维矩阵
private int[][] edges;

// 边的条数
private int numOfEdges;

// 定义boolean数组,标记某个节点是否被访问
private boolean[] isVisited;


/**
* 构造器
*
* @param n 代表图中顶点的个数
*/
public GraphDemo(int n) {
// 初始化二维矩阵
edges = new int[n][n];

// 初始化顶点的ArrayList
vertexList = new ArrayList<>(n);

// 因为还不知道边的关系,所以初始化为0
numOfEdges = 0;

// 记录某个顶点是否被访问
isVisited = new boolean[n];
}

/**
* 返回没有遍历过的相邻节点的下标
*
* @param index 当前节点的下标
* @return 邻接节点的下标
*/
public int getNeighbor(int index) {
// 首先把自己标记为已经遍历
isVisited[index] = true;

// 从头开始遍历,找到能够连接并且没有标记为已遍历的节点
for (int i = 0; i < vertexList.size(); i++) {
if (i == index) {
continue;
}
// 假如找到一个能够连通的,并且没有遍历过的,那么就标记此节点已经遍历并返回对应的坐标
if (edges[index][i] > 0 && !isVisited[i]) {
return i;
}
}

// 假如找不到符合要求的节点,返回-1
return -1;
}

/**
* 图的深度优先遍历
*
* @param index 当前节点
*/
public void DFS(int index) {
// 首先输出当前节点
System.out.println(vertexList.get(index));

// 找到下一个相邻的、可以直接连通、没有遍历过的节点,递归
while (true) {
int neighbor = getNeighbor(index);
if (neighbor == -1) {
return;
}
DFS(neighbor);
}
}

public static void main(String[] args) {
int n = 5;
String[] VertexValue = {"A", "B", "C", "D", "E"};

GraphDemo graph = new GraphDemo(n);

// 添加顶点
Arrays.stream(VertexValue).forEach(graph::insertVertex);

// 添加边,A-C、B-C、B-D、B-E、C-D
graph.insertEdge(0, 2, 1);
graph.insertEdge(1, 2, 1);
graph.insertEdge(1, 3, 1);
graph.insertEdge(1, 4, 1);
graph.insertEdge(2, 3, 1);

// 关系图
graph.showGraph();

System.out.println("-----------------------");

// 深度优先遍历,顺序应该为:A-->C-->B-->D-->E
graph.DFS(0);

}

}

因为东西太多,所以只放出部分,剩下的在之前都有实现


图的广度优先

图的广度优先思想(Broad First Search)

广度优先类似一个分层搜索的功能,需要使用一个队列以保持访问过的节点的顺序。

简单来说,比如说从A开始访问,那么我首先走一遍能和A进行互通的所有节点入队列。

等到A所互通的节点走完之后,就把A从队列里面剔除,从队列的头开始再次进行遍历,遍历到之后就入队列,循环往复。

这个时候,只要队列不为空,就说明还没有结束,假如队列为空,则代表全部都已经遍历完了

广度优先遍历算法步骤

1、访问初始节点v,将v标记为已经访问

2、节点v入队列,当队列非空时,继续执行,否则算法结束

3、查找v的相邻节点,全部入队列,并标记为已经访问

4、v的相邻节点全部遍历完成,则v出队列

5、根据队列中的第一个元素u进行查找相邻节点,循环往复

6、当队列为空时,算法结束

这个广度优先还不算很难,所以不举例子了

广度优先代码实现

image-20210126115438890

按照广度优先遍历,这个最终输出的顺序应该为:A–>C–>B–>D–>E

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
package com.howling;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;

public class GraphDemo {
// 图中顶点的集合
private ArrayList<String> vertexList;

// 图中的顶点之间的连接关系,二维矩阵
private int[][] edges;

// 边的条数
private int numOfEdges;

// 定义boolean数组,标记某个节点是否被访问
private boolean[] isVisited;


/**
* 构造器
*
* @param n 代表图中顶点的个数
*/
public GraphDemo(int n) {
// 初始化二维矩阵
edges = new int[n][n];

// 初始化顶点的ArrayList
vertexList = new ArrayList<>(n);

// 因为还不知道边的关系,所以初始化为0
numOfEdges = 0;

// 记录某个顶点是否被访问
isVisited = new boolean[n];
}

/**
* 返回没有遍历过的相邻节点的下标
*
* @param index 当前节点的下标
* @return 邻接节点的下标
*/
public int getNeighbor(int index) {
// 首先把自己标记为已经遍历
isVisited[index] = true;

// 从头开始遍历,找到能够连接并且没有标记为已遍历的节点
for (int i = 0; i < vertexList.size(); i++) {
if (i == index) {
continue;
}
// 假如找到一个能够连通的,并且没有遍历过的,那么就标记此节点已经遍历并返回对应的坐标
if (edges[index][i] > 0 && !isVisited[i]) {
isVisited[i] = true;
return i;
}
}

// 假如找不到符合要求的节点,返回-1
return -1;
}

/**
* 图的广度优先遍历
*
* @param index 开始的下标
*/
public void BFS(int index) {

// 队列
LinkedList<Integer> queue = new LinkedList<>();

// 首先将初始节点加入到队列中
queue.add(index);

while (!queue.isEmpty()) {
// 查找下一个相邻的、可以直接连通的、没有访问过的节点
int neighbor = getNeighbor(queue.get(0));
// 假如可以找到符合要求的节点
if (neighbor != -1) {
// 将指定的节点加入到队列中
queue.add(neighbor);
}
// 假如可以找到符合要求的节点,那么队列首部出队列并且打印
else {
Integer first = queue.removeFirst();
System.out.print(vertexList.get(first) + " ");
}
}

}

public static void main(String[] args) {
int n = 5;
String[] VertexValue = {"A", "B", "C", "D", "E"};

GraphDemo graph = new GraphDemo(n);

// 添加顶点
Arrays.stream(VertexValue).forEach(graph::insertVertex);

// 添加边,A-C、B-C、B-D、B-E、C-D
graph.insertEdge(0, 2, 1);
graph.insertEdge(1, 2, 1);
graph.insertEdge(1, 3, 1);
graph.insertEdge(1, 4, 1);
graph.insertEdge(2, 3, 1);

// 关系图
graph.showGraph();

System.out.println("-----------------------");

// 深度优先遍历,顺序应该为:A-->C-->B-->D-->E
graph.BFS(0);

}
}

这里小小地修改了getNeighbor,在返回节点之前也进行了一次的改变遍历的状态


广度优先和深度优先比较

image-20210126155051342

在上图中:

1、深度优先:1–>2–>4–>8–>5–>3–>6–>7

2、广度优先:1–>2–>3–>4–>5–>6–>7–>8