图是一个非线性数据结构,本文将讲解图的基本运用,将图巧妙运用,并用TypeScript将其实现,欢迎各位感兴趣的开发者阅读本文。
本文着重讲解图的实现思路,对图的基础概念不了解的开发者,请移步我的另一篇文章:图的认识。
图是网络结构的抽象模型,图是由一组边连接的顶点。任何二元关系都可以用图来表示,例如:社交网络、道路、航班以及通信。
一个图G = (V, E)
由以下元素组成。
下图描述了一个图。
通过上图我们来讲解下图的一些术语。
图可以是无向(没有方向)的或是有向(有向图)的。上面我们画的是无向图,下图描述了一个有向图。
图可以用多种数据结构来表示,不存在绝对正确的方式。图的正确表示法取决于待解决的问题和图的类型。
图最常见的实现是邻接矩阵,每个节点都和一个种整数相关联,该整数将作为数组的索引。我们可以用一个二维数组来表示顶点之间的的连接。如果索引为i的节点和索引为j的节点相邻,则 array[i][j] = 1
,否则 array[i][j] = 0
,如下图所示
不是强联通的图(稀疏图)如果用邻接矩阵来表示,则矩阵中将会有很多0,这意味着我们浪费了计算机存储空间来表示根本不存在的边。例如,找给定顶点的相邻顶点,即使该顶点只有一个相邻顶点,我们也不得不迭代一整行。邻接矩阵表示法不够好的一个理由是:图中顶点的数量可能会改变,而二维数组不太灵活。
我们可以使用临接表这种动态数据结构来表示图,临接表由图中每个顶点的相邻顶点列表所组成。我们可以使用数组、链表、散列表或字典来表示相邻顶点列表,如下图所示描述了临接表这种数据结构。
临接表对大多数问题来说是比较好的选择,以上两种表示法都很有用,他们有着不同的性质(例如,要找出v和w是否相邻,使用邻接矩阵会比较快)。
我们还可以使用关联矩阵来表示图。在关联矩阵中,矩阵的行表示顶点,列表示边。
如下图所示,使用二维数组来表示两者之间的连通性,如果顶点v是边e的入射点,则 array[v][e] = 1
;否则, array[v][e] = 0
。
关联矩阵通常用于边的数量比顶点多的情况,以节省空间和内存。
我们选用临接表来表示图,接下来我们来分析下如何来实现图。
接下来我们需要实现两个方法:一个用来向图中添加一个新的顶点,另一个用来添加顶点之间的边。
向图中添加顶点(addVertex)
向图中添加边(addEdge)
上面我们实现了向图中插入值,我们还需要获取图中的值以及将图转换成比较友好的字符串。
获取图的顶点列表(getVertices)
将图转换为字符串(toString)
前面我们分析了图的实现思路,接下来我们将上述思路转换为代码。
export default class Graph {
// 存储图的顶点
private vertices: (number | string)[] = [];
// 存储临接表
private adjList: Dictionary<string | number, (string | number)[]> = new Dictionary();
constructor(private isDirected: boolean = false) {}
}
// 添加顶点
addVertex(v: string | number): void {
// 顶点不存在于图中
if (!this.vertices.includes(v)) {
// 将该顶点添加到顶点列表中
this.vertices.push(v);
// 在临接表中设置顶点v作为键,对应的字典值为一个空数组
this.adjList.set(v, []);
}
}
// 添加线,连接顶点
addEdge(v: string | number, w: string | number): void {
// 添加顶点之前需要验证顶点v和w是否在图中,不存在就追加
if (!this.adjList.get(v)) {
this.addVertex(v);
}
if (!this.adjList.get(w)) {
this.addVertex(w);
}
// 将w加入到v的临接表中,我们就得到了一条来自顶点v到顶点w的边
this.adjList.get(v)?.push(w);
if (!this.isDirected) {
// 如果是无向图则需要添加一条自w到v的边
this.adjList.get(w)?.push(v);
}
}
// 获取顶点列表
getVertices(): (string | number)[] {
return this.vertices;
}
// 获取临接表
getAdjList(): Dictionary<string | number, (string | number)[]> {
return this.adjList;
}
// 将图转为字符串
toString(): string {
let s = "";
for (let i = 0; i < this.vertices.length; i++) {
s += `${this.vertices[i]} -> `;
const neighbors = <Array<string | number>>this.adjList.get(this.vertices[i]);
for (let j = 0; j < neighbors.length; j++) {
s += `${neighbors[j]} `;
}
s += "\n";
}
return s;
}
完整代码请移步:Graph.ts
接下来我们通过一个例子来测试下我们上述图的实现是否正确,我们还是以基本概念中所用的图为例。
为了方便起见,我们创建了一个数组,这个数组包含了图中的所有顶点,我们遍历数组,将数组中的每个顶点添加进我们的图中。
const graph = new Graph();
const vertices = ["A", "B", "C", "D", "E", "F", "G", "H", "I"];
for (let i = 0; i < vertices.length; i++) {
graph.addVertex(vertices[i]);
}
然后,调用addEdge方法添加我们想要的边。
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方法将图转换为字符串,将其打印到控制台。
console.log("图的关系对应如下");
console.log(graph.toString());
运行结果如下,我们发现图的对应关系与基本概念中的示例图一样。
完整代码请移步:GraphTest.js
与树结构类似,我们可以访问图的所有节点,有两种算法可以实现对图进行遍历:广度优先搜索和深度优先搜索。
图遍历可以用来寻找特定的顶点或寻找连个顶点之间的路径,检查图是否联通,检查图是否含有环。
阅读量:2010
点赞量:0
收藏量:0