图
图的相关术语
图是网络结构的抽象模型。图是一组由边连接的节点。由一条边连接在一起的顶点称为相邻顶点。一个顶点的度是其相邻顶点的数量。路径是顶点的一个连续序列。简单路径要求不包含重复的顶点。环是除去最后一个顶点的简单路径。如果图中每两个顶点之间都存在路径,则该图是连通的。
有向图和无向图
图可以是无向的或是有向的。如果图中每两个顶点间在双向上都存在路径,则该图是强连通的。图还可以是未加权的或是加权的。
图的表示
邻接矩阵
图最常见的实现是邻接矩阵。每个节点都和一个整数相关联,该整数将作为数组的索引。我们用一个二维数组来表示顶点之间的连接。如果索引为i的节点和索引为j的节点相邻,则array[i][j] === 1,否则为0。
不是强连通的图如果用邻接矩阵来表示,则矩阵中将会有很多0,这意味着我们浪费了计算机存储空间来表示根本不存在的边。
邻接表
邻接表由图中每个顶点的相邻顶点列表所组成。可以用列表、链表、散列表、字典来表示相邻顶点列表。
关联矩阵
在关联矩阵中,矩阵的行表示顶点,列表示边。通常用于边的数量比顶点多的情况,以节省空间和内存。
创建Graph类
接收一个参数来表示图是否有向,默认情况下,图是无向的。用一个数组来存储图中所有顶点的名字,用字典来存储邻接表。字典将会使用顶点的名字作为键,邻接顶点列表作为值。
class Graph {constructor(isDirected = false) {this.isDirected = isDirected;this.vertices = [];this.adjList = new Dictionary();}//接着实现两个方法,一个用来向图中添加一个新的顶点,一个用来添加顶点之间的边。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);if(!this.isDirected) {this.adjList.get(w).push(v);}}//取值方法getVertices() {return this.vertices;}getAdjList() {return this.adjList;}toString() {let s = '';for(let i = 0; i < this.vertices.length; i++) {s += `${this.vertices[i]} -> `;const neighbors = this.adjList.get(this.vertices[i]);for(let j = 0; j < neighbors.length; j++) {s += `${neighbors[j]}`;}s += '\n';}return s;}}
addEdge方法接收两个顶点作为参数,需要验证顶点是否存在于图中,不存在则加入顶点列表。
然后通过将w加入到v的邻接表中,我们添加了一条自v到w的边。如果要实现无向图,需要再添加一条反向的边。
图的遍历
算法有广度优先搜索和深度优先搜寻。
图遍历算法的思想是必须追踪每个第一次访问的节点,并且追踪有哪些节点还没有被完全探索。对于两种图遍历算法,都需要明确指出第一个被访问的顶点。
完全探索一个顶点要求我们查看该顶点的每一条边。对于每一条边所连接的没有被访问过的顶点,将其标注为被发现的,并将其加进待访问顶点列表中。
为了保证算法效率,务必访问每个顶点至多两次。连通图中每条边和顶点都会被访问到。
广度优先搜索和深度优先搜寻基本相同,只有一点不同,那就是待访问顶点的数据结构。
| 算法 | 数据结构 | 描述 |
|---|---|---|
| 深度优先搜寻 | 栈 | 将顶点存入栈,顶点是沿着路径被搜索的,存在新的相邻顶点去访问。 |
| 广度优先搜索 | 队列 | 将顶点存入队列,最先入队列的顶点先被搜索。 |
当要标注已经访问过的顶点时,我们用三种颜色反映它们的状态。
白色:表示该顶点还没有被访问。
灰色:被访问过,但并未被搜索过。
黑色:被访问且完全探索过。
这就是之前提到的务必访问每个顶点最多两次的原因。
为了有助于在广度优先和深度优先中标记顶点,我们要使用Colors变量(作为一个枚举器):
const Colors = {WHITE:0,GREY:1,BLACK:2}
两个算法还需要一个辅助对象来帮助存储顶点是否被访问过。在每个算法开头,所有的顶点会被标记为未访问(白色)。我们要用下面的函数初始化每个顶点的颜色。
const initializeColor = vertices => {const color = {};for(let i = 0; i < vertices.length; i++) {color[vertices[i]] = Colors.WHITE;}return color;};
广度优先搜索
会从指定的第一个顶点开始遍历图,先访问其所有的邻点(相邻顶点),就像一次访问图的一层。换句话说就是先宽后深地访问顶点。
以下是从顶点v开始的广度优先搜索算法所遵循的步骤。
- 创建一个队列Q。
- 标注v为被发现的(灰色),并将v入队列Q。
如果Q非空,则运行以下步骤:
- 将u从Q中出队列;
- 标注u为被发现的(灰色);
- 将u所有未被访问过的邻点(白色)入队列;
- 标注u为已被搜索的(黑色)。
export const breadthFirstSearch = (graph, startVertex, callback) => {const vertices = graph.getVertices();const adjList = graph.getAdjList();const color = initializeColor(vertices);const queue = new Queue();queue.enqueue(startVertex);while(!queue.isEmpty()) {const u = queue.dequeue();const neighbors = adjList.get(u);color[u] = Colors.GREY;for(let i = 0; i<neighbors.length; i++) {const w = neighbors[i];if(color[w] === Colors.WHITE) {color[w] = Colors.GREY;queue.enqueue(w);}}color[u] = Colors.BLACK;if(callback) {callback(u);}}}
1.使用BFS寻找最短路径
考虑如何解决以下问题:给定一个图G和源顶点v,找出每个顶点u和v之间最短路径的距离。
对于给定顶点v,广度优先搜索会访问所有与其距离为1的顶点,接着是距离为2的顶点,以此类推。所以,可以用广度优先算法解决这个问题,可以修改上面的算法:
从v到u的距离distances[u];前溯点predecessors[u],用来推导出从v到其他每个顶点u的最短路径。
const BFS = (graph, startVertex) => {const vertices = graph.getVertices();const adjList = graph.getAdjList();const color = initializeColor(vertices);const queue = new Queue();const distances = {};const predecessors = {};queue.enqueue(startVertex);for(let i = 0; i < vertices.length; i++) {//用0初始化数组distances[vertices[i]] = 0;predecessors[vertices[i]] = null;}while(!queue.isEmpty()) {const u = queue.dequeue();const neighbors = adjList.get(u);color[u] = Colors.GREY;for(let i = 0; i<neighbors.length; i++) {if(color[w] === Colors.WHITE) {color[w] = Colors.GREY;distances[w] = distances[u] + 1;predecessors[w] = u;queue.enqueue(w);}}color[u] = Colors.BLACK;}return {distances, predecessors};}
深度优先搜寻
深度优先搜寻会从第一个指定的顶点开始遍历图,沿着路径直到这条路径最后一个顶点被访问了,接着原路回退并探索下一条路径。换句话说,它是先深度后广度地访问顶点。
深度优先搜索不需要一个源顶点,若图中顶点v未访问,则访问该顶点v。
要访问顶点v,需要:
- 标注v为被发现的(灰色);
- 对于v的所有未访问(白色)的邻点w,访问顶点w;
- 标注v为已被探索的(黑色)。
深度优先搜索的步骤是递归的,这意味着深度优先搜索算法使用栈来存储函数调用。
//深度优先搜索const depthFirstSearch = (graph, callback) => {const vertices = graph.getVertices();const adjList = graph.getAdjList();const color = initializeColor(vertices);for (let i = 0; i < vertices.length; i++) {if (color[vertices[i]] === Colors.WHITE) {depthFirstSearchVisit(vertices[i], color, adjList, callback);}}};const depthFirstSearchVisit = (u, color, adjList, callback) => {color[u] = Colors.GREY;if (callback) {callback(u);}const neighbors = adjList.get(u);for (let i = 0; i < neighbors.length; i++) {const w = neighbors[i];if (color[w] === Colors.WHITE) {depthFirstSearchVisit(w, color, adjList, callback);}}color[u] = Colors.BLACK;};
