图在JavaScript中常用邻接表表示,适合稀疏图和动态操作,邻接矩阵适用于顶点固定且边密集的场景,边列表则用于特定算法;实际应用如社交网络、导航和推荐系统均依赖图结构。

图,简单来说,就是由一些“点”(我们称之为顶点或节点)和连接这些点的“线”(我们称之为边)构成的抽象结构。它最核心的作用是用来描述事物之间的关系。在JavaScript中,表示图结构最常见也最灵活的方式是使用邻接表(Adjacency List),当然,邻接矩阵(Adjacency Matrix)和边列表(Edge List)也是可选的方案,具体用哪种,得看你的实际需求和图的特性。
在JavaScript中表示图结构,我个人最常用且推荐的是邻接表。它本质上是一个映射(Map或Object),每个键代表一个顶点,而对应的值通常是一个数组或Set,里面存储着与该顶点直接相连的所有邻居顶点。这种方式对于稀疏图(边相对较少)来说,在空间效率上表现出色。
1. 邻接表 (Adjacency List)
class Graph {
constructor() {
this.adjList = new Map(); // 使用Map来存储邻接表,键是顶点,值是Set(方便快速去重和查找)
}
addVertex(vertex) {
if (!this.adjList.has(vertex)) {
this.adjList.set(vertex, new Set()); // 新增顶点,并初始化一个空的邻居集合
}
}
addEdge(vertex1, vertex2, isDirected = false) {
// 确保两个顶点都存在
if (!this.adjList.has(vertex1)) {
this.addVertex(vertex1);
}
if (!this.adjList.has(vertex2)) {
this.addVertex(vertex2);
}
this.adjList.get(vertex1).add(vertex2); // 添加从vertex1到vertex2的边
if (!isDirected) { // 如果是无向图,需要双向添加
this.adjList.get(vertex2).add(vertex1);
}
}
removeVertex(vertex) {
if (!this.adjList.has(vertex)) return;
// 移除所有指向该顶点的边
for (let [v, neighbors] of this.adjList.entries()) {
neighbors.delete(vertex);
}
// 移除顶点自身
this.adjList.delete(vertex);
}
removeEdge(vertex1, vertex2, isDirected = false) {
if (this.adjList.has(vertex1) && this.adjList.get(vertex1).has(vertex2)) {
this.adjList.get(vertex1).delete(vertex2);
}
if (!isDirected && this.adjList.has(vertex2) && this.adjList.get(vertex2).has(vertex1)) {
this.adjList.get(vertex2).delete(vertex1);
}
}
getNeighbors(vertex) {
return this.adjList.get(vertex) || new Set();
}
printGraph() {
for (let [vertex, neighbors] of this.adjList.entries()) {
console.log(`${vertex} -> ${[...neighbors].join(', ')}`);
}
}
}
// 示例使用
// const myGraph = new Graph();
// myGraph.addVertex('A');
// myGraph.addVertex('B');
// myGraph.addEdge('A', 'B');
// myGraph.addEdge('B', 'C');
// myGraph.addEdge('A', 'C', true); // 有向边 A -> C
// myGraph.printGraph();
/*
A -> B, C
B -> A, C
C -> B
*/2. 邻接矩阵 (Adjacency Matrix)
当图的顶点数量固定且边非常密集时,邻接矩阵可能是一个不错的选择。它使用一个二维数组
matrix[i][j]
i
j
// 假设顶点是0到n-1的数字
class AdjacencyMatrixGraph {
constructor(numVertices) {
this.numVertices = numVertices;
this.matrix = Array(numVertices).fill(0).map(() => Array(numVertices).fill(0));
}
addEdge(v1, v2, weight = 1, isDirected = false) {
if (v1 < 0 || v1 >= this.numVertices || v2 < 0 || v2 >= this.numVertices) {
console.error("Invalid vertex index.");
return;
}
this.matrix[v1][v2] = weight;
if (!isDirected) {
this.matrix[v2][v1] = weight;
}
}
hasEdge(v1, v2) {
if (v1 < 0 || v1 >= this.numVertices || v2 < 0 || v2 >= this.numVertices) {
return false;
}
return this.matrix[v1][v2] !== 0;
}
getWeight(v1, v2) {
if (this.hasEdge(v1, v2)) {
return this.matrix[v1][v2];
}
return null;
}
// ... 其他操作,比如移除边、获取邻居等,都围绕这个二维数组展开
}3. 边列表 (Edge List)
最简单粗暴的方式,就是直接列出所有的边,每条边通常表示为一个包含两个顶点(和可能的权重)的数组。这种方式在图算法的某些特定阶段可能会用到,比如Kruskal算法(需要对边进行排序),但对于图的遍历和邻居查询则效率较低。
// const edgeList = [
// ['A', 'B'],
// ['B', 'C'],
// ['A', 'C', { weight: 5, type: 'friend' }] // 也可以存储额外信息
// ];说实话,我一直觉得图是计算机科学里最优雅也最实用的抽象之一,它把复杂的关系网变得一目了然。你可能每天都在和图打交道,只是没意识到。比如,我们用的各种社交网络,像微信朋友圈、微博关注,它们的关系链条就是典型的图结构,每个人是一个节点,关注或好友关系就是边。推荐系统也是图的天下,它会分析你和商品、你和朋友、朋友和商品之间的关系,然后给你推荐可能喜欢的东西。
再比如,导航系统,从A点到B点怎么走最快?这不就是找图上最短路径的问题吗?每个路口是节点,每段路是边,边的权重可以是距离或时间。甚至我们日常的项目管理,任务之间的依赖关系,哪个任务必须先完成,哪个可以并行,这都可以用图来清晰地表示和调度。还有软件工程里的依赖管理(比如npm包之间的依赖),编译器的AST(抽象语法树),数据库的关系模型,这些背后都有图论的影子。理解图,就像是掌握了一种描述世界底层逻辑的强大工具。
这是一个很实际的问题,我在做项目时也经常权衡。其实没有绝对的“最好”,只有“最适合”。
邻接表的优势在于:
但它的缺点是:
邻接矩阵的优势在于:
matrix[i][j]
它的缺点是:
我的选择偏好: 通常情况下,我个人更倾向于使用邻接表。原因很简单,大多数实际应用中的图都是稀疏的,而且对动态性有要求。比如社交网络,你不可能预先知道会有多少用户,以及用户之间会有多少边。只有当你明确知道图的顶点数量是固定的,并且图非常密集(比如完全图),或者你需要频繁地执行“查询任意两点间是否有边”这种操作时,我才会考虑邻接矩阵。
图的基本操作除了上面提到的添加/删除顶点和边,最核心的莫过于图的遍历了。理解遍历是掌握图算法的关键一步,因为很多复杂的图问题(比如最短路径、连通分量)都是在遍历的基础上进行的。
1. 广度优先搜索 (BFS - Breadth-First Search)
BFS就像在水面上扩散的波纹,它会一层一层地访问节点,先访问起始节点的所有邻居,再访问这些邻居的邻居,以此类推。它通常用于寻找最短路径(在无权图中)或者遍历所有可达节点。
// 基于前面定义的Graph类
Graph.prototype.bfs = function(startVertex) {
const visited = new Set();
const queue = [startVertex]; // 使用数组模拟队列
visited.add(startVertex);
while (queue.length > 0) {
const currentVertex = queue.shift(); // 取出队列头部元素
console.log(`Visited: ${currentVertex}`); // 访问当前节点
for (const neighbor of this.getNeighbors(currentVertex)) {
if (!visited.has(neighbor)) {
visited.add(neighbor);
queue.push(neighbor); // 将未访问的邻居加入队列
}
}
}
};
// 示例:
// const bfsGraph = new Graph();
// bfsGraph.addEdge('A', 'B');
// bfsGraph.addEdge('A', 'C');
// bfsGraph.addEdge('B', 'D');
// bfsGraph.addEdge('C', 'E');
// bfsGraph.addEdge('D', 'F');
// bfsGraph.bfs('A'); // 输出顺序可能是 A, B, C, D, E, F2. 深度优先搜索 (DFS - Depth-First Search)
DFS则像是在迷宫里探险,它会尽可能深地探索一条路径,直到无路可走,然后回溯,再探索另一条路径。DFS通常用于检测环、拓扑排序、寻找连通分量等。
// 基于前面定义的Graph类
Graph.prototype.dfs = function(startVertex) {
const visited = new Set();
const _dfs = (vertex) => {
visited.add(vertex);
console.log(`Visited: ${vertex}`); // 访问当前节点
for (const neighbor of this.getNeighbors(vertex)) {
if (!visited.has(neighbor)) {
_dfs(neighbor); // 递归访问未访问的邻居
}
}
};
_dfs(startVertex);
};
// 示例:
// const dfsGraph = new Graph();
// dfsGraph.addEdge('A', 'B');
// dfsGraph.addEdge('A', 'C');
// dfsGraph.addEdge('B', 'D');
// dfsGraph.addEdge('C', 'E');
// dfsGraph.addEdge('D', 'F');
// dfsGraph.dfs('A'); // 输出顺序可能是 A, B, D, F, C, E (取决于邻居的迭代顺序)实现这些操作时,通常会用到一个
visited
以上就是图的定义是什么?JS如何表示图结构的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号