概述
本系列最终目的是按照Dagoba: An In-Memory Graph Database教程实现一个内存型图数据库,故本文先从最基本的图的输入、构建、搜索开始。
因为“根据输入构建图”和“在图中搜索”是图数据库的核心功能之二。
程序功能描述
功能列表:
- 根据输入的边列表构建图
- 输入起始顶点和目标顶点,搜索所有连通的路径
输入格式
边列表:
const edges: number[][] = [[1, 2], [1, 3], [2, 4], [2, 5], [3, 6], [3, 7], [4, 8]
, [4, 9], [5, 10], [5, 11], [6, 12], [6, 13], [7, 14], [7, 15]];
构建图
图有很多种表示方式,其中邻接矩阵通常比较稀疏,浪费空间,所以更紧凑的邻接列表是更实用的表示方式。
由于用List结构实现邻接列表的话,检索第一层每个顶点的时间复杂度是O(n);但是如果用Map结构来实现邻接列表的话,检索第一层顶点的时间复杂度就降到了O(1)。
/**
* 简单的图
*/
class SimpleGraph {
/**
* 边列表
*/
private edges: number[][];
/**
* 邻接表
* 这里为了简化操作,将原始邻接表的list数据结构换成了Map数据结构
*/
private adjacencyMap: Map<number, number[]> = new Map();
/**
* 构造器
* @param edges 边列表
*/
constructor(edges: number[][]) {
this.edges = edges;
this.init();
}
/**
* 初始化
*/
init() {
this.edges.forEach(edge => {
// 起始顶点
const from: number = edge[0];
// 目标顶点
const to: number = edge[1];
// 如果该起始顶点邻接顶点数组未初始化则先初始化
if (!this.adjacencyMap.has(from)) {
this.adjacencyMap.set(from, []);
}
// 将连通顶点放入邻接数组
this.adjacencyMap.get(from).push(to);
});
}
}
使用序列化语句输出this.adjacencyMap
的结果:
console.log(JSON.stringify(Array.from(this.adjacencyMap.entries())));
// [[1,[2,3]],[2,[4,5]],[3,[6,7]],[4,[8,9]],[5,[10,11]],[6,[12,13]],[7,[14,15]]]
搜索图
图最常见的功能之一就是搜索两点之间可达的所有路径,下面就用广度优先搜索和深度优先搜索分别实现一次。
广度优先搜索
代码实现
/**
* 广度优先搜索
* @param from 起始顶点
* @param to 目标顶点
* @returns 从起始节点出发所有可达目标节点路径
*/
breadthFirstSearch(from: number, to: number): number[][] {
// 结果路径列表
const resultPaths: number[][] = [];
// 待遍历的路径列表,初始化时将起始节点作为第一个路径
const toVisitPaths: number[][] = [[from]];
// 当还有路径需要遍历时
while (toVisitPaths.length > 0) {
// 当前路径,从待遍历路径队列中拉出一条路径
const curPath: number[] = toVisitPaths.shift() || [];
// 获取该路径中最近一个遍历的节点
const latestNode = curPath[curPath.length - 1];
// 获取该节点可达的节点
const nextNodes = this.adjacencyMap.get(latestNode) || []
for (let nextNode of nextNodes) {
// 如果当前路径已遍历过该节点,则跳过,避免环
if (curPath.indexOf(nextNode) >= 0) {
continue;
}
// 将下一个节点压入路径,以作为下一个要遍历的节点
curPath.push(nextNode);
if (nextNode === to) {
// 如果找到了目标节点,则深拷贝路径到结果路径中,并不再加入待遍历路径列表
resultPaths.push(curPath.slice());
} else {
// 如果还不是目标节点,则添加到待遍历路径列表
toVisitPaths.push(curPath.slice());
}
// 弹出刚刚的节点以还原路径
curPath.pop();
}
}
return resultPaths;
}
运行结果
运行代码:
// 边列表
const edges: number[][] = [[1, 2], [1, 3], [2, 4], [2, 5], [3, 6], [3, 7], [4, 8]
, [4, 9], [5, 10], [5, 11], [6, 12], [6, 13], [7, 14], [7, 15]];
// 构建图
const simpleGraph: SimpleGraph = new SimpleGraph(edges);
// 在图中搜索路径
console.log(JSON.stringify(simpleGraph.breadthFirstSearch(1, 15)));
// 遍历顺序: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// 输出结果: [[1,3,7,15]]
深度优先搜索
代码实现
/**
* 深度优先搜索
* @param from 起始节点
* @param to 目标节点
* @returns 从起始节点出发所有可达目标节点路径
*/
depthFirstSearch(from: number, to: number): number[][] {
// 结果路径列表
const resultPaths: number[][] = [];
// 待遍历的路径
const toVisitPaths: number[][] = [[from]];
while (toVisitPaths.length > 0) {
// 从栈中弹出最近队列作为当前路径
const curPath: number[] = toVisitPaths.pop() || [];
// 最近一个遍历的节点
const latestNode = curPath[curPath.length - 1];
// 获取该节点可达的节点
const nextNodes = this.adjacencyMap.get(latestNode) || [];
for (let nextNode of nextNodes) {
// 如果当前路径已遍历过该节点,则跳过,避免环
if (curPath.indexOf(nextNode) >= 0) {
continue;
}
// 将下一个节点压入路径,以作为下一个要遍历的节点
curPath.push(nextNode);
if (nextNode === to) {
// 如果找到了目标节点,则深拷贝路径到结果路径中,并不再加入待遍历路径列表
resultPaths.push(curPath.slice());
} else {
// 如果还不是目标节点,则添加到待遍历路径列表
toVisitPaths.push(curPath.slice());
}
// 弹出刚刚的节点以还原路径
curPath.pop();
}
}
return resultPaths;
}
运行结果
// 边列表
const edges: number[][] = [[1, 2], [1, 3], [2, 4], [2, 5], [3, 6], [3, 7], [4, 8]
, [4, 9], [5, 10], [5, 11], [6, 12], [6, 13], [7, 14], [7, 15]];
// 构建图
const simpleGraph: SimpleGraph = new SimpleGraph(edges);
// 在图中搜索路径
console.log(JSON.stringify(simpleGraph.depthFirstSearch(1, 15)));
// 遍历顺序:1 2 3 6 7 14 15 12 13 4 5 10 11 8 9
// 输出结果:[[1,3,7,15]]
小结
可以看到深搜和广搜的区别就仅仅在于用“待遍历的路径”toVisitPaths
获取下一个遍历路径curPath
的方式:
- 广度优先搜索用的是
shift()
,即把toVisitPaths
当做队列,先进先出。 - 深度优先搜索用的是
pop()
,即把toVisitPaths
当做栈,后进先出。
完整代码见文章顶部CSDN免费附件