1. 图
1.1了解图
图是网状结构的抽象模型,图示由一组由边连接的节点.它由节点边组成,节点之间的由边连接,一个节点可以对应很多.(这些边的数量称为这个节点度).相邻节点的顺序组成的序列叫做路径.路径中由没有重复节点的叫做简单路径.路径中最后一个顶点和第一个节点相同的构成环,有向图的边带箭头的(有序的).每个节点都有边的图强连通图.
我们还可以使用图来表示道路、航班以及通信,下面是一个非常简单的图
下面我们来介绍图的一些基本概念
由一些边连接在一起的顶点称为相邻顶点,比如,在上图中A和B相邻的, A和C相邻的,A和E是不相邻的.
一个顶点的度是其相邻顶点的数量, 比如,A和其他两个顶点相连接,因此A的度为2.
路径是顶点v1,v2,...v3的一个连续序列,其中vi和vi+1是相邻的,以上的示意图中,其中包含路径是ABE和ACG
简单路径要求不包含重复的顶点,举个例子,ACG就是一个简单路径.除去最后一个顶点(因为它和第一个顶点是同一个顶点),环也是一个简单路径,比如ABCA(最后一个顶点重新回到A).
1.2 创建图
要储存图中的节点, 并且表明节点间边的关系,我们可以采用多种方法来实现.最常见的有邻接矩阵和邻接表
邻接矩阵
邻接矩阵的属性比较复杂,每个节点都和一个整数相关联,该整数将作为数组的索引.我们用一个二维数组来表示顶点之间的连接.如果索引为i的节点和j相邻,则array[i][j] === 1
,否者array[i][j] === 0
.如下图所示
假设我们有如下的图
此时对应的邻接矩阵如下图:
邻接矩阵使用二维数组来描述节点间的连接关系.但是当节点比边多很多时,此时我们可以很明显的看到矩阵中将会有很多0, 这意味着我们浪费了计算机的储存空间来表示了根本不存在的边.例如,当我们要找给定顶点的相邻顶点,我们也不得不迭代一整行.邻接矩阵表示不够好的另一个理由是,图中的顶点可能会改变,而二维数组不太灵活.
邻接表
我们也可以使用一种叫做邻接表的方法来表示图.邻接表由图中每个顶点的相邻顶点列表组成.存在好几种发生来表示这种数据结构.我们可以用列表(数组),链表,甚至是哈希表或是字典来表示相邻顶点表.下面的列表展示了邻接表的数据结构.
- A: B C
- B: D
- C: B
- D: B
接下来我们使用链接表的方法来创建图
// 图结构 类
class Graph {
constructor() {
// 储存所有的节点
this.vertices = [];
// 储存每个顶点对应的相邻顶点
this.edges = {}
}
// 添加顶点
addVertex(...rest) {
rest.map(v => {
// 如果顶点已经添加 return
if (this.vertices.includes(v)) return
//顶点不存在 添加顶点
this.vertices.push(v);
// 初始化顶点的边储存信息
this.edges[v] = [];
})
}
// 添加边
addEdge(v, w) {
// 如果不存在对应的节点, 则添加到图中
if(!this.vertices.includes(v) || !this.vertices.includes(w)){
this.addVertex(v, w);
}
// 如果两个顶点之间已经存在相邻关系,则返回
if (this.edges[v].includes(w)) return;
//两个顶点之间相互添加边信息
this.edges[v].push(w);
this.edges[w].push(v);
}
}
接下来我们来简单测试一下这个代码
let graph = new Graph();
graph.addVertex('A','B',"C",'D');
graph.addEdge('A','B')
graph.addEdge('B', 'C')
graph.addEdge('B', 'D')
graph.addEdge('C', 'D');
graph.addEdge('C', 'A');
console.log(graph)
为了更方便的我们调试,我们来实现Graph类的toString方法,以便在控制台来输出图
toString(){
let s = "";
for(let vertice of this.vertices){
s += `${vertice} --->`;
for(let edg of this.edges[vertice]){
s += `${edg}`;
}
s += '\n'
}
return s
}
我们为邻接表表示法构建了一个字符串, 首先迭代vertices数组列表,将其每一项的顶点追加到字符串中,接着我们通过这个顶点取出该顶点的所有邻接表,同样我们迭代该邻接表,并将其相邻顶点追加到我们的字符串中,链接表迭代完成后,我们该该字符串添加一个换行符,这样我们可以在控制台看到应该漂亮的输出.此时控制台输入如下:
A --->BC
B --->ACD
C --->BDA
D --->BC
1.3 图的遍历
和树的数据结构类似, 我们也可以实现访问图中的所有节点.有两种算法可以实现对图的遍历:广度优先搜索
和深度优先搜索
.图的遍历可以用来寻找特定的节点或者两个顶点之间的路径,检测图是否连通,检测图是否含有环,等等,
图遍历算法的核心思想是必须追踪每一次访问的节点,并且追踪有哪些节点还没有被完全探索.对于两种图的遍历算法.都需要明确指出第一次被访问的节点.
当要标注的节点已经访问过的节点时, 此时我们需要用三种颜色来反映它们的状态.
- 白色: 表示该节点还没有被访问过.
- 灰色: 表示该节点被访问过, 但还未被探索过.
- 黑色: 表示该顶点被访问过且被完全探索过.
const Colors = {
WHITE:0,
CRET:1,
BLACK:2,
}
为了有助于在广度优先和深度优先算法中标记顶点,我们使用Colors变量(作为一个枚举器),声明如下,
两个算法实现之前还需要一个辅助对象来帮助储存顶点是否被储存过.在每个算法的前头,所有的顶点会被标记为未访问.我们使用下面的函数来初始化每个顶点的颜色.
const initializeColor = vertices =>{
let color = {};
for(let i=0,len=vertices.length;i
1.3.1广度优先遍历
广度优先搜索算法会从指定的第一次顶点开始遍历,先访问其所有的邻点,就像一次访问图的一层.换句话来说,就是先宽后深地访问顶点.
广度优先遍历和深度优先遍历的算法基本都相同,只有一点不同,那就是待访问顶点的数据结构.广度优先遍历使用的是数据结构为队列.下面我们来简单地来实现一个队列类.代码如下.
// 单向队列
class Queue {
constructor() {
this.queue = []
}
//入队
enqueue(...items){
return items.map(item => {
this.queue.push(item);
return item
})
}
// 出队
dequeue(){
return this.queue.shift()
}
// 返回队头
first(){
return this.queue[0];
}
//清空队列
clear(){
this.queue = [];
}
// 返回队列的长度
size(){
return this.items.length;
}
//队列是否为空
isEmpty(){
return this.size() === 0
}
}
以下是从顶点v开始的广度优先搜索算法的核心步骤
- 创建一个队列Q.
- 将顶点v标注为灰色,并加入队列.
- 如果队列q非空. 则进行以下遍历步骤.
- 从队列Q中取出u;
- 标注u为被发现的(灰色);
- 将所有未被访问的的邻点(白色)加入队列;
- 标注顶点u为已被探索的(黑色)
下面我们来简单实现广度优先搜索算法.为了方便, 我们直接在Graph类中实现一个bfs(breadthFirstSearch)方法.
bfs(startVertex,callback){
const vertices = this.vertices;
const edges = this.edges;
const color = initializeColor(vertices);
const queue = new Queue();
// 入口顶点入队
quene.enQueue(startVertex)
//循环队列进行发现或探索
while(!quene.isEmpty()){
// 取出队首顶点
const u = queue.deQueue();
// 获取顶点对应的相邻顶点
const neighbors = edges[u];
//标注顶点为灰色
color[u] = Colors.GREY;
//探索顶点u的所有邻点列表
neighbors.forEach(w =>{
// 如果邻点不为白色,说明节点已经被访问过了, 则不进行任何操作
if(color[w] !== Colors.WHITE) return;
// 将u所有未被访问的节点加入队列, 并标注为灰色
quene.enQueue(w);
color[w] = Colors.GREY
})
// 探索结束,改变状态为BLACK
color[u] = Colors.BLACK;
// 如果存在callback, 则将顶点u传入参数传递到callback
if(callback){
callback(u)
}
}
}
上面的bfs方法接受一个顶点和可选的callback函数方便来访问广度优先搜索到的每一个节点,在实现bfs算法之前我们第一次事情就是用initialize函数来将color数组初始化为白色.我们还需要声明一个和创建一个Quene实例,它将会储存代访问和待探索的节点.
接下来我们来用之前的图实例来简单测试一下这个方法
graph.bfs('A',(v) =>{
console.log(v)
})
// 依次在控制台打印 A B C D
1.3.2使用bfs寻找最短路径
利用bfs我们可以输出访问顶点的顺序,但是bfs的用途远不止如此, 我们还可以使用bfs来找寻最短路径,例如我们考虑如下问题.
给定一个图G和源顶点v,找出顶点u和v之间最短路径的距离.
对于给定的顶点v,广度优先算法会访问所有的与其距离为1的顶点,接着是距离为2的顶点.所以我们可以使用广度优先算法来解决这个问题.我们可以稍微修改一下我们之前写的bfs方法以返回给我们一些信息.
- 从v到u的距离distances[u]
- 从v到u的具体路径
BFS(v){
const vertices = this.vertices;
const edges = this.edges;
const color = initializeColor(vertices);
const queue = new Queue();
const info = {}
// 入口顶点入队, 状态改变, 初始信息
queue.enqueue(v);
color[v] = Colors.GREY;
info[v] = {distance:0, path:v}
//发现和探索
while(!queue.isEmpty()){
//获取顶点和其所有邻点
const u = queue.dequeue();
const neighbors = edges[u];
color[u] = Colors.GREY;
neighbors.forEach(w =>{
if(color[w] !== Colors.WHITE) return;
color[w] = Colors.GREY;
//记录对应的邻点w和u组之间的距离和路径
info[w] = {
distance:info[u].distance+1,
path:info[u].path + '->' + w
}
queue.enqueue(w)
})
color[u] = Colors.GREY
}
return info
}
当我们对顶点u的邻点进行探索时,我们会判断当前邻点是否被访问过, 如果没有被访问过, 此时我们将邻点的状态设置为被访问过,并记录邻点w和u之间的距离.我们还通过info[u] .distance+ 1 `来增加u和w之间的距离,并且将w的顶点u的路径path和w连接累积起来,并记录在info[w]上.
let graph = new Graph;
graph.addEdge("A" , "B");
graph.addEdge("A" , "C");
graph.addEdge("A" , "E");
graph.addEdge("B" , "D");
graph.addEdge("B" , "E");
graph.addEdge("C" , "F");
graph.addEdge("E" , "F");
console.log(graph.BFS("A"));
此时我们会得到如下输出
{
A: {distance: 0, path: "A"}
B: {distance: 1, path: "A->B"}
C: {distance: 1, path: "A->C"}
D: {distance: 2, path: "A->B->D"}
E: {distance: 1, path: "A->E"}
F: {distance: 2, path: "A->C->F"}
}
这意味着顶点A与B、C、E的距离为1, 与顶点D、F的距离为2. 对应的路径在path字段中, 我们可以很方便的看到对应的路径信息.
1.3.3深度优先搜索算法
深度优先搜索算法将会从第一个指定的顶点开始遍历图,沿着路径直到这条路径最后一个顶点被访问了, 接着原路回退并探索下一条路径.换句话来说,它是先深度后广度地访问节点.
深度优先搜索算法不需要一个源顶点,在深度优先搜索算法中,若图中的顶点v未访问,则访问该顶点v,要访问顶点v,照如下步骤做:
- 标注v为被发现的(灰色)
- 对于v的所有被访问的(白色)的邻点w,访问顶点w.
- 标注v为已被探索的(黑色).
深度优先搜索的步骤为是递归的,这意味值深度优先搜索算法使用栈来储存函数调用(由递归调用所创建的栈)
接下来我们来简单实现深度优先算法
// 递归探索顶点
const dfsWalk = (u,color,edgs,callback) =>{
// 将顶点u状态设为灰色(已发现)
color[u] = Colors.GREY;
if(callback) callback(u);
// 获取顶点对应的邻点列表
const neighbors = edgs[u];
// 探索顶点的所有邻点
neighbors.forEach(w =>{
// 如果邻点w状态为白色(未被访问), 则递归调用dfsWalk, 添加顶点w入栈
if(w !== Colors.WHITE) return;
dfsWalk(w,color,edgs,callback)
})
// 探索结束, 将状态设置黑色(已被探索)
color[u] = Colors.BLACK
}
//深度优先搜索
const dfs = (graph,callback) =>{
// 浅拷贝图中的所有顶点和邻点列表
const vertices = {...graph.vertices};
const edges = {...graph.edges};
const color = initializeColor(vertices);
//探索所有的顶点
Object.values(vertices).forEach(v =>{
dfsWalk(v,color,edges,callback)
})
}
接下来来调用dfs函数
dfs(graph,(v) =>{
console.log(v)
})
依次在控制台输出 A B C E D F