1. 图的介绍
- 图是一组由边连接的节点(或顶点)
- 任何二元关系都可以用图来表示
- 任何社交网络,例如Facebook、Twitter和Google+,都可以用图来表示;还可以使用图来表示道路、航班以及通信等
概念:
一个图G=(V, E)由以下元素组成:
- V:一组顶点
- E:一组边,连接V中的顶点
相邻顶点: 由一条边连接在一起的顶点。如A和B相邻,A和E不相邻
度: 一个顶点的度是其相邻顶点的数量。如A和其他三个顶点相连接,则A的度为3
路径: 顶点v 1, v2,…, vk的一个连续序列,其中vi和vi+1是相邻的
简单路径: 简单路径要求不包含重复的顶点。A、D、G是一条简单路径;环也是一个简单路径
有向图:
- 有向图的边有一个方向
- - 如果图中每两个顶点间在双向上都存在路径,则该图是强连通的。例如,C和D是强连通的,而A和B不是强连通的
- 加权图和未加权图:
2. 图的表示
2.1 邻接矩阵
图常用邻接矩阵来实现。每个节点都和一个整数相关联,该整数将作为数组的索引。
用一个二维数组来表示顶点之间的连接。如果索引为i的节点和索引为j的节点相邻,则array[i][j] === 1,否则array[i][j] === 0
缺点:
- 不是强连通的图(稀疏图)如果用邻接矩阵来表示,则矩阵中将会有很多0,这意味着会浪费计算机存储空间来表示根本不存在的边。例如,找给定顶点的相邻顶点,即使该顶点只有一个相邻顶点,我们也不得不迭代一整行。
- 图中顶点的数量可能会改变,而二维数组不太灵活。
2.2 邻接表
2.3 关联矩阵
在关联矩阵中,矩阵的行表示顶点,列表示边。
关联矩阵通常用于边的数量比顶点多的情况,以节省空间和内存。
3. Graph类
先声明类的骨架:
class Graph {
constructor(isDirected = false) {
// Graph构造函数可以接收一个参数来表示图是否有向,默认情况下是无向的
this.isDirected = isDirected;
// 使用数组来存储图中所有顶点的名字
this.vertices = [];
// 用字典来存储邻接表
this.adjList = new Dictionary();
}
}
实现向图中添加一个新的顶点的方法:
addVertex(v) {
// 接收顶点v作为参数,这个顶点不存在于图中时
if (!this.vertices.includes(v)) {
// 将该顶点添加到顶点列表中
this.vertices.push(v);
// 在邻接表中,设置顶点v作为键对应的字典值为一个空数组
this.adjList.set(v, []);
}
};
实现添加顶点之间的边的方法:
addEdge(v, w) {
// 如果顶点v不存在于图中,将它加入顶点列表
if (!this.adjList.get(v)) {
this.addVertex(v);
}
// 如果顶点w不存在于图中,将它加入顶点列表
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() {
return this.vertices;
}
声明返回邻接表的方法:
getAdjList() {
return this.adjList;
}
测试代码:
const graph = new Graph();
const myVertices = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'];
for (let i = 0; i < myVertices.length; i++) {
graph.addVertex(myVertices[i]);
}
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');
console.log(graph.toString());
实现Graph类的toString方法,以便在控制台输出图:
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;
}
结果:
4. 图的遍历
对图进行遍历的算法:
- 广度优先算法
- 深度优先算法
图遍历可以用来寻找特定的顶点或寻找两个顶点之间的路径,检查图是否连通,检查图是否含有环等。
图遍历的思想:
- 图遍历算法必须追踪每个第一次访问的节点,并追踪有哪些节点还没被完全探索
- 两种算法都需要明确指出第一个被访问的顶点
- 完全探索一个顶点要求查看该顶点的每一条边
- 对于每一条边所连接的没有被访问过的顶点,将其标注为被发现的,并将其加进待访问顶点列表中
- 为了保证算法的效率,务必访问每个顶点至多两次
- 连通图中每条边和顶点都会被访问到
两种算法的区别:
算法 | 数据结构 | 描述 |
---|---|---|
深度优先算法 | 栈 | 将顶点存入栈,顶点是沿着路径被探索的,存在新的相邻顶点就去访问 |
广度优先算法 | 队列 | 将顶点存入队列,最先入队列的顶点先被探索 |
要标注已经访问过的顶点时,可以用三种颜色来反映他们的状态:
- 白色:该顶点还没被访问
- 灰色:该顶点被访问过,但并未被探索过
- 黑色:该顶点被访问过且被探索过
需要使用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;
}
4.1 广度优先搜索
广度优先搜索算法会从指定的第一个顶点开始遍历图,先访问其所有的邻点(相邻顶点),就像一次访问图的一层,也就是先宽后深地访问顶点。
步骤:
- 创建一个队列Q
- 标注v为被发现的(灰色),并将v入队列Q
- 如果Q非空,则:
- 将u从Q中出队列
- 标注u为被发现的(灰色)
- 将u所有未访问过的邻点(白色)入队列
- 标注u为已被探索的(黑色)
算法实现:
const breadthFirstSearch = (graph, startVertex, callback) => {
const vertices = graph.getVertices();
const adjList = graph.getAdjList();
// 用initializeColor函数将color数组初始化为白色
const color = initializeColor(vertices);
// 声明和创建一个队列来存储待访问和待探索的顶点
const enqueue = new Queue();
// 将顶点入队列
queue.enqueue(startVertex);
// 如果队列非空
while (!queue.isEmpty()) {
// 通过出队列操作从队列中移除一个顶点
const u = queue.dequeue();
// 取得一个包含其所有邻点的邻接表
const neighbors = adjList.get(u);
// 将该顶点标注为灰色,表示发现但未探索该顶点
const [u] = Colors.GREY;
for (let i = 0; i < neighbors.length; i++) {
// 对于u的每个邻点,需要取得其值
const w = neighbors[i];
// 如果它还没被访问过
if (color[w] === Colors.WHITE) {
// 标注已经发现了它,设为灰色
color[w] = Colors.GREY;
// 将该顶点入队列
queue.enqueue(w);
}
}
// 当完成探索该顶点和其相邻顶点后,将该顶点标注为已探索过的,即黑色
color[u] = Colors.BLACK;
// breadthFirstSearch方法接收一个回调。这个参数是可选的,如果传递了回调函数,就会用到它
if (callback) {
callback(u);
}
}
}
测试代码:
const printVertex = (value) => console.log('Visited vertex: ' + value);
breadthFirstSearch(graph, myVertices[0], printVertex);
结果:
1. 使用BFS寻找最短路径
给定一个图G和源顶点v,找出每个顶点u和v之间的最短路径的距离。
对于给定顶点v,广度优先算法会先访问所有与其距离为1的顶点,接着是距离为2的顶点,以此类推。
- 从v到u的距离 distances[u]
- 前溯点 predecessors[u],用来推导出从v到其他每个顶点u的最短路径
改进的广度优先方法:
const BFS = (graph, startVertex) => {
const verteices = graph.getVertices();
const adjList = graph.getAdjList();
const color = initializeColor(verteices);
const queue = new Queue();
// 表示距离
const distances = {};
// 表示前溯点
const predecessors = {};
queue.enqueue(startVertex);
// 对于图中的每一个顶点
for(let i = 0; i < verteices.length; i++) {
// 用0和null来初始化
distances[verteices[i]] = 0;
predecessors[verteices[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++) {
const w = neighbors[i];
if (color[w] === CVolors.WHITE) {
color[w] = Colors.GREY;
// 增加v和w之间的距离
distances[w] = distances[u] + 1;
// 设置w的前溯点为u
predecessors[w] = u;
queue.enqueue(w);
}
}
color[u] = Colors.BLACK;
}
// 返回一个包含distances和predecessors的对象
return {
distances,
predecessors
}
}
4.2 深度优先搜索
从第一个指定的顶点开始遍历图,沿着路径直到这条路径最后一个顶点被访问了,接着原路回退并探索下一条路径。
深度优先搜索算法不需要一个源顶点。
在深度优先搜索算法中,若图中顶点v未访问,则访问该顶点v。
步骤:
- 标注v为被发现的(灰色)
- 对于v的所有未访问(白色)的邻点w,访问顶点w
- 标注v为已被探索的(黑色)
// 深度优先搜索
// 接收一个Graph类实例和回调函数作为参数
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) {
// 传递的参数为 要访问的顶点u,颜色数组以及回调函数
depthFirstSearchVisit(vertices[i], color, adjList, callback);
}
}
};
const depthFirstSearchVisit = (u, color, adjList, callback) => {
// 访问顶点u时标注其为发现的
color[u] = Colors.GREY;
// 如果有callback函数,就执行该函数输出已访问过的顶点
if (callback) {
callback(u);
}
// 取得包含顶点u所有邻点的列表
const neighbors = adjList.get(u);
// 对于顶点u的每一个未访问过的邻点w,调用depthFirstSearchVisit函数,传递w和其他参数
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;
}
如果希望深度优先算法遍历图G的所有节点,构建“森林”以及一组源顶点,并输出两个数组:发现时间和完成探索时间。
- 顶点u的发现时间d[u]
- 当顶点u被标注为黑色时,u的完成探索实践时间f[u]
- 顶点u的前溯点p[u]
const DFS = graph => {
const vertices = graph.getVertices();
const adjList = graph.getAdjList();
const color = initializeColor(vertices);
const d = {};
const f = {};
const p = {};
// 追踪发现时间和完成探索时间
const time = { const: 0 };
// 声明d、f、p
for (let i = 0; i < vertices.length; i++) {
f[vertices[i]] = 0;
d[vertices[i]] = 0;
p[vertices[i]] = null;
}
for (let i = 0; i < vertices.length; i++) {
if (color[vertices[i]] === Colors.WHITE) {
DFSVisit(vertices[i], color, d, f, p, time, adjList);
}
}
// 为图的每一个顶点初始化数组,并在这个方法结尾处返回这些值
return {
discovery: d,
finished: f,
predecessors: p
}
};
const DFSVisit = (u, color, d, f, p, time, adjList) => {
color[u] = Colors.GREY;
// 当一个顶点第一次被发现时,追踪其发现时间
d[u] = ++time.count;
const neighbors = adjList.get(u);
for (let i = 0; i < verteices.length; i++) {
if (color[w] === Colors.WHITE) {
// 追踪前溯点
p[w] = u;
DFSVisit(w, color, d, f, p.time, adjList);
}
}
color[u] = Colors.BLACK;
// 顶点完全被探索后,追踪其完成时间
f[u] = ++time.count;
}