图
在熟悉了图的基本概念之后,本文使用ts实现图数据结构上常用的方法。在这些方法中,对图遍历的两种方式,深度优先和广度优先是难点和重点! 本文的例子中使用的是没有方向的无权图。
1. 图中的方法
-
- addVertex(v: string): void -> 向图中添加顶点的方法
-
- addEdge(v1: string, v2: string): void -> 向图中添加边的方法
-
- removeVertex (v: string): boolean -> 删除图中的某一个顶点
-
- removeEdge(v1: string, v2: string): void -> 删除图中的某个边
-
- initial(vertexes: string[]): void -> 通过一个数组来初始化图上的顶点
-
- toString(): string -> 以邻接表的格式打印出图
-
- DFSVisit(firstVertex: string, handler: Function): void -> 根据指定的入口和处理方式深度遍历图结构
-
- BFSVisit(firstVertex: string, handler: Function): void -> 根据指定的入口和处理方式广度遍历图结构
2. 方法实现思路
- addVertex(v: string)
往结点数组中新添加一个元素,然后在字典中增加此结点对应的空数组。
addVertex(v: string): void {
this.vertexes.push(v);
this.edges.set(v, []);
}
- addEdge(v1: string, v2: string)
首先在字典中找到v1对应的数组,然后检查此数组中是否已经有v2了,如果有的话证明已经建立关系了,但是没有的话就将v2加入到此数组的尾部;然后对v2进行完全相同的操作。
addEdge(v1: string, v2: string): void {
const v1Set = this.edges.get(v1);
const index = v1Set?.findIndex((v) => v === v2);
if (index === -1) {
v1Set?.push(v2);
}
const v2Set = this.edges.get(v2);
const index2 = v2Set?.findIndex((v) => v === v1);
if (index2 === -1) {
v2Set?.push(v1);
}
}
- removeVertex (v: string)
首先在结点数组中找到此结点然后删除,如果没有找到说明根本没有插入进来过;删除之后将此结点对应的字典中的数组也要删除;不仅如此还要遍历其他结点对应字典中的数组,从这些数组中挨个删除结点v。
removeVertex (v: string): boolean {
const index = this.vertexes.findIndex(
_v => _v === v
)
if (index === -1) {
return false;
} else {
this.vertexes.splice(index, 1);
this.vertexes.forEach(
_v => {
const _vSet = this.edges.get(_v);
const _index = _vSet?.findIndex(
_v => _v === v
)
if (_index !== undefined && _index !== -1) {
_vSet!.splice(_index, 1);
}
}
)
this.edges.delete(v);
return true;
}
}
- removeEdge(v1: string, v2: string)
先找到v1结点对应的字典中的数组,在此数组中删除v2;同样再找到v2结点对应的字典中的数组,在此数组中删除v1;如果顺利的话就返回true,表示删除成功,否则返回false表示没有找到,删除不成功!
removeEdge(v1: string, v2: string): void {
const v1Set = this.edges.get(v1);
const index = v1Set?.findIndex((v) => v === v2);
if (index !== undefined && index !== -1) {
v1Set?.splice(index, 1);
}
const v2Set = this.edges.get(v2);
const index2 = v2Set?.findIndex((v) => v === v1);
if (index2 !== undefined && index2 !== -1) {
v2Set?.splice(index2, 1);
}
}
- initial(vertexes: string[])
遍历入参数组,再遍历过程中将数组中元素作为addVertex方法的入参插入进去即可!
initial(vertexes: string[]): void {
vertexes.forEach((v) => {
this.addVertex(v);
});
}
- toString()
通过两层遍历即可,外层遍历的是顶点数组,拿到数组中的某个元素之后,去字典中找到对应的数组,再次对此数组进行遍历即可!
toString(): string {
let rst = "";
this.vertexes.forEach((v) => {
const _tmp = this.edges.get(v);
rst += v;
_tmp?.forEach((e) => {
rst += "->".concat(e);
});
rst += "\n";
});
return rst;
}
- DFSVisit(firstVertex: string, handler: Function)
见下面的小节5.
- BFSVisit(firstVertex: string, handler: Function)
见下面的小节5.
3. DFS和BFS
DFS(深度优先搜索)和 BFS(广度优先搜索)是图遍历算法,用于遍历或搜索图中的节点。
-
- DFS(深度优先搜索):
- DFS 从一个起始顶点开始,沿着一条路径尽可能深入地访问未被访问过的节点,直到到达最深处的节点或无法继续前进。然后回溯到上一个节点,继续探索其他路径,重复这个过程,直到遍历完所有的节点。
- 在实际应用中,DFS 使用递归或栈数据结构来实现,通过记录已经访问过的节点以及访问顺序,对节点进行深度优先遍历。
- DFS 的特点是在搜索过程中能够尽可能深入地搜索,适用于解决连通性、路径和拓扑排序等问题。
-
- BFS(广度优先搜索):
- BFS 从一个起始顶点开始,首先访问起始顶点的所有邻居节点,然后再依次访问它们的邻居节点,层层向外扩展,直到没有新的节点可以访问为止。
- 在实际应用中,BFS 使用队列数据结构来实现,通过记录已经访问过的节点以及访问顺序,对节点进行广度优先遍历。
- BFS 的特点是按照距离起始节点的距离逐层遍历,适用于解决最短路径、连通性和最小生成树等问题。
-
- 比较:
- DFS 是一种先深入后回溯的遍历策略,而 BFS 是一种逐层扩展的遍历策略。
- DFS 在搜索过程中可能会沿着某条路径一直深入下去,直到到达最深处,然后回溯;而 BFS 则是按照层次逐步扩展,先访问离起始节点更近的节点。
- DFS 有可能陷入无限循环,需要合理设置终止条件;BFS 则保证了在有限时间内遍历完所有可达节点。
- DFS 的空间复杂度较小,但找到的路径不一定是最短路径;BFS 对于求解最短路径问题非常有效,但需要更多的存储空间。
- 选择使用 DFS 还是 BFS 取决于具体的问题需求。如果要找到最短路径或者需要按照距离进行遍历,可以选择 BFS。如果要探索图的结构、寻找连通性或拓扑排序,可以选择 DFS。
4. 染色
- 染色的本质是通过为结点添加额外的状态来保证在遍历的时候做到不重不漏。
- 三种颜色对应三种状态,颜色0表示此节点尚未被访问过;颜色1表示此节点被访问过,但是其邻居没有被完全访问,也可以简单记为:未被探索;颜色为2表示此节点和其邻居结点全部被访问过。
- 染色的实现是通过一个map记录每个顶点的访问记录实现的;在开始访问之前需要将这个map初始化出来,然后保证在遍历过程中都能对其时刻进行访问。
5. DFS和BFS实现分析
5.1 DFS
深度优先遍历使用的是递归,形式上十分的简洁,实现步骤如下:
- 标记为1
- 进行访问
- 找到邻居
- 遍历访问标记为0的邻居
- 标记为2
5.2 BFS
广度优先遍历使用的是队列和循环,形式上也十分的简洁,实现步骤如下:
- 初始化颜色
- 创建队列
- 首结点入队列
- 队列循环开始,直到队列中没有元素
- 每一个循环中做的事情
- 5.1. 取出队头元素
- 5.2. 访问队头元素
- 5.3. 找到队头元素的所有邻居
- 5.4. 遍历所有找到的邻居
- 5.5. 如果邻居的颜色为0,那么久将其改成1然后放入到队尾去
- 5.6. 遍历完邻居之后,将此结点的颜色变成2
6. 代码
class _Graph {
// 顶点数组,用来保存图上的顶点
vertexes: string[] = [];
// 边字典,使用字典保存顶点和边之间的关系
edges: Map<string, string[]> = new Map();
// 添加顶点的方法
addVertex(v: string): void {
this.vertexes.push(v);
this.edges.set(v, []);
}
// 无向图添加边的方法
// 需要解决重复插入的问题
addEdge(v1: string, v2: string): void {
const v1Set = this.edges.get(v1);
const index = v1Set?.findIndex((v) => v === v2);
if (index === -1) {
v1Set?.push(v2);
}
const v2Set = this.edges.get(v2);
const index2 = v2Set?.findIndex((v) => v === v1);
if (index2 === -1) {
v2Set?.push(v1);
}
}
// 删除顶点的方法
removeVertex (v: string): boolean {
const index = this.vertexes.findIndex(
_v => _v === v
)
if (index === -1) {
return false;
} else {
this.vertexes.splice(index, 1);
this.vertexes.forEach(
_v => {
const _vSet = this.edges.get(_v);
const _index = _vSet?.findIndex(
_v => _v === v
)
if (_index !== undefined && _index !== -1) {
_vSet!.splice(_index, 1);
}
}
)
this.edges.delete(v);
return true;
}
}
// 删除某条边的方法
removeEdge(v1: string, v2: string): void {
const v1Set = this.edges.get(v1);
const index = v1Set?.findIndex((v) => v === v2);
if (index !== undefined && index !== -1) {
v1Set?.splice(index, 1);
}
const v2Set = this.edges.get(v2);
const index2 = v2Set?.findIndex((v) => v === v1);
if (index2 !== undefined && index2 !== -1) {
v2Set?.splice(index2, 1);
}
}
// 布置顶点
initial(vertexes: string[]): void {
vertexes.forEach((v) => {
this.addVertex(v);
});
}
// 打印邻接表
toString(): string {
let rst = "";
this.vertexes.forEach((v) => {
const _tmp = this.edges.get(v);
rst += v;
_tmp?.forEach((e) => {
rst += "->".concat(e);
});
rst += "\n";
});
return rst;
}
// 图的遍历
// 深度优先遍历
DFSVisit(firstVertex: string, handler: Function): void {
const colors = this.initializeColor(); // 初始化顶点颜色
this.DFSVisitHelper(firstVertex, colors, handler);
}
// 对于深度优先遍历,颜色为0表示没有被访问过,颜色为1表示被访问过,但是其邻居还没有被全部访问,颜色为2表示该结点的所有邻居都已经被访问过了
// 深度优先遍历使用的是递归,形式上十分的简洁
/**
* 1. 标记为1
* 2. 进行访问
* 3. 找到邻居
* 4. 遍历访问标记为0的邻居
* 5. 标记为2
*/
private DFSVisitHelper(
vertex: string,
colors: Record<string, 0 | 1 | 2>,
handler: Function
) {
colors[vertex] = 1; // 标记当前顶点为已访问
handler(vertex); // 处理当前顶点
const neighbors = this.edges.get(vertex); // 获取当前顶点的邻居顶点
if (neighbors) {
for (const neighbor of neighbors) {
if (colors[neighbor] === 0) {
this.DFSVisitHelper(neighbor, colors, handler);
}
}
}
colors[vertex] = 2; // 标记当前顶点的所有邻居顶点都已访问完毕
}
// 广度优先遍历
// 对于广度优先遍历,颜色为0表示没有被访问过,颜色为1表示被访问过,但是其邻居还没有被全部访问,颜色为2表示该结点的所有邻居都已经被访问过了
// 广度优先遍历使用的是队列和循环,形式上也十分的简洁
/**
* 1. 初始化颜色
* 2. 创建队列
* 3. 首结点入队列
* 4. 队列循环开始,直到队列中没有元素
* 5. 每一个循环中做的事情
* 5.1 取出队头元素
* 5.2 访问队头元素
* 5.3 找到队头元素的所有邻居
* 5.4 遍历所有找到的邻居
* 5.5 如果邻居的颜色为0,那么久将其改成1然后放入到队尾去
* 5.6 遍历完邻居之后,将此结点的颜色变成2
*/
BFSVisit(firstVertex: string, handler: Function): void {
const colors = this.initializeColor(); // 初始化顶点颜色
const queue: string[] = []; // 使用队列存储待访问的顶点
colors[firstVertex] = 1; // 标记第一个顶点为已访问
queue.push(firstVertex); // 将第一个顶点加入队列
while (queue.length > 0) {
const currentVertex = queue.shift() as string; // 取出队列的头部顶点
handler(currentVertex); // 处理当前顶点
const neighbors = this.edges.get(currentVertex); // 获取当前顶点的邻居顶点
if (neighbors) {
for (const neighbor of neighbors) {
if (colors[neighbor] === 0) {
colors[neighbor] = 1; // 标记邻居顶点为已访问
queue.push(neighbor); // 将邻居顶点加入队列
}
}
}
colors[currentVertex] = 2; // 标记当前顶点的所有邻居顶点都已访问完毕
}
}
// 结点初始化着色方法
initializeColor(): Record<string, 0 | 1 | 2> {
const colors: Record<string, 0 | 1 | 2> = {};
this.vertexes.forEach((v) => {
colors[v] = 0;
});
return colors;
}
}
// 测试
const g = new _Graph();
const vers = ["A", "B", "C", "D", "F"];
g.initial(vers);
g.addEdge("A", "B");
g.addEdge("A", "C");
g.addEdge("A", "F");
g.addEdge("B", "F");
g.addEdge("C", "D");
g.addEdge("F", "D");
g.addEdge("B", "D");
g.addEdge("B", "C");
g.addEdge("C", "F");
console.log(g.toString());
/**
A->B->C->F
B->A->F->D->C
C->A->D->B->F
D->C->F->B
F->A->B->D->C
*/
g.DFSVisit("B", console.log);
/*
B A C D F
*/
g.BFSVisit("B", console.log);
/**
B A F D C
*/
g.removeEdge("B","F");
console.log(g.toString());
/**
A->B->C->F
B->A->D->C
C->A->D->B->F
D->C->F->B
F->A->D->C
*/
g.removeVertex("C");
console.log(g.toString());
/**
A->B->F
B->A->D
D->F->B
F->A->D
*/