这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战
图
图是网络结构的抽象模型。图是一组边连接的节点,一个图 G = (V, E) 由以下元素组成,V:一组顶点; E:一组边,连接V中的顶点。在我们生活中可以使用图来表示道路、网络通信、航班等等。
图的基本术语
- 相邻顶点:由一组边连接起来的顶点
- 度:一个顶点的度是其相邻顶点的数量
- 路径:是顶点V1,V2, V3,...,Vn的一个连续序列,其中Vi和V(i+1)是相邻的。简单路径要求不包含重复节点
- 有向图和无向图:图可以是有向的,也可以是无向的,有向图的边有一个方向。如果图中的每两个顶点间在双向上都存在路径,那么图是强相通的。
- 加权图和未加权的:加权的边会被赋予权值。
图的表示
图最常见的实现是邻接矩阵,除此之外,我们还可以用邻接表、关联矩阵来实现。在这节我们主要来讨论用邻接表来实现他。
创建图Graph
先介绍一下邻接表,邻接表由图中每个顶点的相邻顶点列表所组成。简单表示如下,左边表示我们的节点,右边是该节点的相邻节点。
A: B C D
B: A E F
C: A D G
...
I: E
根据上面的结构我们可以来创建我们所需要的图结构了
class Graph {
constructor(isDirected = false) {
this.isDirected = isDirected; //是否是有向图,默认是无向的
this.vertices = []; // 用来存我们的节点
// 用来存储我们的邻接表,用字典实现,键为顶点,值为他的相邻顶点列表
this.adjList = new Map()
}
}
创建好我们的图结构后,我们需要添加两个方法,一个方法是addVertex(v)函数,用来添加我们的节点,另一个是addEdge(v, w)函数,用来添加我们的边。
addVertex(v) {
// 添加的节点不存在,添加进去,并且将他的相邻节点设为 【】
if (!this.vertices.includes(v)) {
this.vertices.push(v)
this.adjList.set(v, [])
}
}
// 添加两节点的边,建立起联系
addEdge(v, w) {
// 两节点不存在,先添加进去,在进行边的关联
if (!this.adjList.get(v)) {
this.addVertex(v)
}
if (!this.adjList.get(w)) {
this.addVertex(w)
}
this.adjList.get(v).push(w) // v ==> w
// 无向图的话,两者互为相邻节点
if (!this.isDirected) {
this.adjList.get(w).push(v) // w ==> v
}
}
除此之外,我们还需要两个函数,用来获取顶点列表和邻接表
// 返回顶点列表
getVertices() {
return this.vertices;
}
// 返回邻接表
getAdjList() {
return this.adjList;
}
接下来,我们可以来写点测试代码,看看我们写的对不对
const graph = new Graph();
const vertices = ["A", "B", "C", "D", "E", "F", "G", "H", "I"]
for (let index = 0; index < vertices.length; index++) {
graph.addVertex(vertices[index])
}
graph.addEdge("A", "B")
graph.addEdge("A", "C")
graph.addEdge("A", "D")
graph.addEdge("C", "D")
graph.addEdge("C", "G")
graph.addEdge("D", "G")
graph.addEdge("D", "H")
graph.addEdge("B", "E")
graph.addEdge("B", "F")
graph.addEdge("E", "I")
然后为了我们能更加清楚的看到我们构建的图对不对,可以实现一个toString方法来进行输出
toString() {
let resultStr = '';
this.vertices.forEach(key => {
resultStr = resultStr + `${key} => ${this.adjList.get(key).join(' ')} \n`
})
return resultStr;
}
然后调用graph.toString()输出结果如下,符合我们的预期,至此,我们的图数据结构也基本实现了。接下来我们要实现图的遍历方法了,这也是重要的内容。
A => B C D
B => A E F
C => A D G
D => A C G H
E => B I
F => B
G => C D
H => D
I => E
图的遍历
和树相似,我们可以访问图中的所有节点,有两种方法来对图进行遍历:广度优先搜索(BFS)和深度优先搜素(DFS)。图的遍历可以用来寻找特定顶点或寻找两个顶点之间的路径,检查图是否有环,是否连通等。
在此之前,我们可以理解一下图遍历的思想。图遍历算法的思想是必须追踪每个第一次访问的节点,并且追踪有哪些节点还没有被完全探索,这两种算法都必须指出第一个被访问的顶点。
两种算法基本相同,不同的是存储待访问列表的数据结构不同,BFS是队列,DFS是栈。除此之外,我们还需要一个辅助方法,来标识顶点是否被访问过。
const Colors = {
WHITE: 0, // 白色:该节点没有被访问
GREY: 1, // 灰色: 被访问过,没有被完全探索
BLACK: 2 // 被完全探索了
}
同时,我们也需要定义一个方法来初始化我们的节点,用来记录节点状态,哪些节点被访问了,哪些被完全探索了,开始都设置为白色。
const initColor = vertices => {
let color = {};
vertices.forEach(v => {
color[v] = Colors.WHITE;
})
return color;
}
接下来就可以来实现我们的BFS和DFS了。
广度优先搜索(BFS)
广度优先搜素会从指定的一个顶点开始遍历图,先访问所有相邻节点,访问图的一层,在下一层。
实现步骤:
- 创建一个队列Q
- 标注节点v为被发现的(灰色),推入v进队列Q
- 队列不为空时
- 将u从队列中取出
- 将u变为灰色
- 遍历所以没有被访问的相邻节点(白色),推入队列
- 标注u为被完全探索的(黑色)
/**
*
* @param {*} graph 图的实例
* @param {*} startEle 从哪个节点开始遍历
* @param {*} callback 遍历一个节点后进行的操作
*/
const bfs = (graph, startEle, callback) => {
const vertices = graph.getVertices();
const adjList = graph.getAdjList();
const color = initColor(vertices);
const queue = []; // 用数组来模拟队列
queue.push(startEle);
while(queue.length !== 0) {
const currentEle = queue.shift()
color[currentEle] = Colors.GREY; // 被访问到了,还没有被完全探索,置为灰色
// 遍历所有相邻节点
const elements = adjList.get(currentEle);
elements.forEach(ele => {
// 相邻节点还没有被访问到,那就添加到对列
if (color[ele] === Colors.WHITE) {
color[ele] = Colors.GREY;
queue.push(ele)
}
})
// 相邻节点都访问了,那么该节点就被完全探索了,变为黑色
color[currentEle] = Colors.BLACK;
// 执行完一轮,一个节点被完全探索了
if (callback) callback(currentEle)
}
}
const log = ele => { console.log('Visited: ', ele) }
bfs(graph, vertices[0], log)
/**
* Visited: A
Visited: B
Visited: C
Visited: D
Visited: E
Visited: F
Visited: G
Visited: H
Visited: I
*/
深度优先搜索(DFS)
深度优先搜素会从指定的一个顶点开始遍历图,沿着路径直到这条路径的最后一个顶点被访问了,按原路回退并探索下一个路径。就是先深度后广度的访问顶点。所以DFS是递归的,意外着我们要用栈来存储他。
const defVisit = (node, color, adjList, callback) => {
// 将该节点变成一访问,未完全探索
color[node] = Colors.GREY;
// 执行回调,说明当前走到了那个节点
if (callback) callback(node);
// 遍历所有相邻节点
const elements = adjList.get(node);
elements.forEach(ele => {
if (color[ele] === Colors.WHITE) {
defVisit(ele, color, adjList, callback)
}
})
color[node] = Colors.BLACK;
}
const dfs = (graph, callback) => {
const vertices = graph.getVertices();
const adjList = graph.getAdjList();
const color = initColor(vertices);
for (let index = 0; index < vertices.length; index++) {
// 遍历每一个节点,如果没有被访问,那么从该节点开始递归该节点
if (color[vertices[index]] === Colors.WHITE) {
// 递归
defVisit(vertices[index], color, adjList, callback)
}
}
}
dfs(graph, log)
/**
* Visited: A
Visited: B
Visited: E
Visited: I
Visited: F
Visited: C
Visited: D
Visited: G
Visited: H
*/
广度优先搜索和深度优先搜索有着多种场景应用,最常见的有最短路径算法等,大家可以下去之后了解了解。