图的定义
维基百科这样定义图结构:图(英语:graph)是一种抽象数据类型,用于实现数学中图论的无向图和有向图的概念。
图的数据结构包含一个有限(可能是可变的)的集合作为节点集合,以及一个无序对(对应无向图)或有序对(对应有向图)的集合作为边(有向图中也称作弧)的集合。节点可以是图结构的一部分,也可以是用整数下标或引用表示的外部实体。
图的数据结构还可能包含和每条边相关联的数值(edge value),例如一个标号或一个数值(即权重,weight;表示花费、容量、长度等)。
通俗的说,图由一组顶点和一组边组成,边是顶点和顶点之间的连线,边可以是有向的也可以是无向的。
七桥问题
七桥问题是18世纪著名的数学问题之一,问题是:由七座桥连接起来的小河两岸及两个岛,一个人怎么才能不重复、不遗漏地一次走完七座桥,最后回到出发点
该问题由著名数学家欧拉给出答案:该问题无解。并给出了连通图可以一笔画的充要条件:
- 奇点的数目不是0个就是2个
- 连到一点的边的数目如果是奇数条,就称为奇点;如果是偶数条就称为偶点
- 想要一笔画成,必须中间点均是偶点,奇点只能在两端,因此任何图能一笔画成,奇点要么没有要么在两端
图的表示
在程序中如何表示图结构呢?图是由顶点和边构成,对于顶点我们可以使用数组保存,对于边则有两种表示方式:邻接矩阵和邻接表
邻接矩阵
邻接矩阵让每个节点和一个整数项关联,该整数作为数组的下标值,我们用一个二维数组来表示顶点之间的连接。如下图所示:
如果两个顶点之间被直接连接,则二维矩阵中对应的值就是1,反之为0。ps:A与B、C、D直接连接,所以arr[0][1]、arr[0][2]、arr[0][3]都是1。通过二维数组,我们可以很快的找到一个顶点和哪些顶点有连线。
邻接矩阵有一个比较严重的问题:当图比较稀疏的时候,二维数组中有着大量的0,非常浪费内存空间。
邻接表
邻接表由图中每个顶点以及和顶点相邻的顶点列表组成,这个顶点列表可以是数组/链表/哈希表等。如下图所示
邻接表的问题:邻接表计算出度是比较简单的(出度:指向别人的数量,入度:指向自己的数量),但是计算入度是一件非常麻烦的事情。所以一般构造一个“逆邻接表”用来计算入度。
图的封装
我们使用邻接表的方式来封装,邻接表使用Set,目的是可以自动去重。图的定义:
class Graph<T> {
// 顶点
vertices: T[] = [];
// 边 用邻接表存储
private adjList: Map<T, Set<T>> = new Map();
}
添加顶点和边
添加顶点很简单,只需要往vertices中push的同时,adjList set一个新数组
// 添加顶点
addVertex(v: T) {
if (!this.vertices.includes(v)) {
this.vertices.push(v);
// 初始化邻接表
this.adjList.set(v, new Set());
}
}
添加边,入参为两个顶点:
// 添加边 v和w之间添加边
addEdge(v: T, w: T) {
if (!this.adjList.get(v)) {
this.addVertex(v);
}
if (!this.adjList.get(w)) {
this.addVertex(w);
}
this.adjList.get(v)!.add(w);
this.adjList.get(w)!.add(v);
}
打印
// 打印
traverse() {
this.vertices.forEach(v => {
console.log(`${v} -> ${Array.from(this.adjList.get(v)!).join(' ')}`)
})
}
图的遍历
图的遍历需要保证每个顶点都要被访问一遍且不能重复访问,有两种遍历方式:广度优先搜索(BFS)和深度优先搜索(DFS),两种方法都要明确执行第一个被访问的顶点。
广度优先搜索
BFS是从根节点开始,沿着树的宽度遍历树的节点。如果所有节点均被访问,则算法中止。这种搜索算法可以使用队列来完成,就像树的层序遍历一样。
// 广度优先遍历
// 广度优先搜索
bfs() {
// 判断有无顶点
if (!this.vertices.length) return;
// 创建一个队列
const queue: T[] = [];
// 创建一个Set来保存已经被加入到队列中的顶点
const pushed = new Set<T>();
// 将第一个顶点加入到队列
queue.push(this.vertices[0]);
// 添加到visited
pushed.add(this.vertices[0])
// 遍历队列
while(queue.length) {
const vertex = queue.shift()!;
// 访问
console.log(vertex)
// 获取vertex的邻接表
const adjList = this.adjList.get(vertex)!
// 将邻接表中没有被访问过的顶点添加到队列
adjList.forEach(vertexItem => {
if (!pushed.has(vertexItem)) {
queue.push(vertexItem)
pushed.add(vertexItem)
}
})
}
}
深度优先搜索
深度优先搜索会尽可能深地搜索树的分支。当节点v的所在边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。
深度优先搜索可以使用栈来完成,思路其实跟广度优先搜索类似,只是把队列换成了栈:
// 深度优先搜索
dfs() {
// 判断有无顶点
if (!this.vertices.length) return;
// 创建一个Set用来记录已经入栈的顶点
const pushed = new Set<T>();
// 创建一个栈
const stack: T[] = [];
// 先将第一个顶点入栈
stack.push(this.vertices[0]);
pushed.add(this.vertices[0]);
while(stack.length) {
// 出栈
const vertex = stack.pop()!;
console.log(vertex);
// 获取邻接表
const adjList = this.adjList.get(vertex)!;
// 为了确保访问顺序是添加的顺序 需要翻转一下邻接表 保证先建立联系顶点先出栈
const adjListReverse = Array.from(adjList).reverse()
adjListReverse.forEach(vertexItem => {
if (!pushed.has(vertexItem)) {
stack.push(vertexItem)
pushed.add(vertexItem)
}
})
}
}
以上就是图结构的简单封装