0

0

图的定义是什么?JS如何表示图结构

小老鼠

小老鼠

发布时间:2025-08-25 11:08:01

|

890人浏览过

|

来源于php中文网

原创

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

图的定义是什么?js如何表示图结构

图,简单来说,就是由一些“点”(我们称之为顶点或节点)和连接这些点的“线”(我们称之为边)构成的抽象结构。它最核心的作用是用来描述事物之间的关系。在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
之间是否存在边(通常用1表示有边,0表示无边,或者存储边的权重)。

// 假设顶点是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(抽象语法树),数据库的关系模型,这些背后都有图论的影子。理解图,就像是掌握了一种描述世界底层逻辑的强大工具

邻接表和邻接矩阵,我到底该选哪个?

这是一个很实际的问题,我在做项目时也经常权衡。其实没有绝对的“最好”,只有“最适合”。

邻接表的优势在于:

TayCMS免费企业建站系统1.8 for PHP
TayCMS免费企业建站系统1.8 for PHP

由于精力有限,程序更新比较慢,请大家谅解,再次感谢支持taycms的朋友们,虽然比较慢,我们还是会一直更新下去的。谢谢您的关注。有什么建议可以到论坛提出,或者直接给我QQ留言。 2.0会有很多新功能,请关注官方论坛TayCMS 1.8 升级日志此版本修复了不少BUG1.更换图片切换JS , 不会再有错误提示2.增加资料下载模块3.更换默认模版,使程序功能和页面结构更清晰,方便参考制作模版4.修复留

下载
  • 空间效率高: 对于稀疏图(边数远小于顶点数的平方),它只存储实际存在的边,所以占用的内存更少。想一下,如果一个图有1000个节点,但每人只认识10个朋友,用邻接矩阵就需要1000x1000的二维数组,而邻接表只存储1000个节点和1000x10个边。
  • 动态性好: 添加或删除顶点、边都相对灵活,不需要像邻接矩阵那样频繁调整大小或重建。
  • 遍历邻居快: 获取一个顶点的所有邻居非常直接,直接访问对应的列表即可。

但它的缺点是:

  • 判断两点间是否有边稍慢: 需要遍历一个顶点的邻居列表,最坏情况下是O(度)的时间复杂度。

邻接矩阵的优势在于:

  • 查询效率高: 判断任意两点之间是否有边,是O(1)的操作,直接访问
    matrix[i][j]
    就行。
  • 实现简单: 对于固定数量的顶点,用二维数组表示直观且易于实现。

它的缺点是:

  • 空间效率低: 对于稀疏图,它会存储大量的0,造成空间浪费。
  • 动态性差: 如果需要添加或删除顶点,通常需要重新构建整个矩阵,这在JS中尤为麻烦。

我的选择偏好: 通常情况下,我个人更倾向于使用邻接表。原因很简单,大多数实际应用中的图都是稀疏的,而且对动态性有要求。比如社交网络,你不可能预先知道会有多少用户,以及用户之间会有多少边。只有当你明确知道图的顶点数量是固定的,并且图非常密集(比如完全图),或者你需要频繁地执行“查询任意两点间是否有边”这种操作时,我才会考虑邻接矩阵。

实际操作中,如何用JS实现图的常见操作?

图的基本操作除了上面提到的添加/删除顶点和边,最核心的莫过于图的遍历了。理解遍历是掌握图算法的关键一步,因为很多复杂的图问题(比如最短路径、连通分量)都是在遍历的基础上进行的。

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, F

2. 深度优先搜索 (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
集合来跟踪已经访问过的节点,避免无限循环(尤其是在有环图中)。而队列(BFS)和递归调用栈(DFS)是实现这两种遍历的核心机制。实际项目里,你可能还会遇到带权图(边有权重)、有向图(边有方向)、多重图(两点间有多条边)等更复杂的场景,但核心的表示和遍历思想是相通的,只需在边的存储和遍历逻辑上做相应调整。

相关专题

更多
js获取数组长度的方法
js获取数组长度的方法

在js中,可以利用array对象的length属性来获取数组长度,该属性可设置或返回数组中元素的数目,只需要使用“array.length”语句即可返回表示数组对象的元素个数的数值,也就是长度值。php中文网还提供JavaScript数组的相关下载、相关课程等内容,供大家免费下载使用。

557

2023.06.20

js刷新当前页面
js刷新当前页面

js刷新当前页面的方法:1、reload方法,该方法强迫浏览器刷新当前页面,语法为“location.reload([bForceGet]) ”;2、replace方法,该方法通过指定URL替换当前缓存在历史里(客户端)的项目,因此当使用replace方法之后,不能通过“前进”和“后退”来访问已经被替换的URL,语法为“location.replace(URL) ”。php中文网为大家带来了js刷新当前页面的相关知识、以及相关文章等内容

374

2023.07.04

js四舍五入
js四舍五入

js四舍五入的方法:1、tofixed方法,可把 Number 四舍五入为指定小数位数的数字;2、round() 方法,可把一个数字舍入为最接近的整数。php中文网为大家带来了js四舍五入的相关知识、以及相关文章等内容

754

2023.07.04

js删除节点的方法
js删除节点的方法

js删除节点的方法有:1、removeChild()方法,用于从父节点中移除指定的子节点,它需要两个参数,第一个参数是要删除的子节点,第二个参数是父节点;2、parentNode.removeChild()方法,可以直接通过父节点调用来删除子节点;3、remove()方法,可以直接删除节点,而无需指定父节点;4、innerHTML属性,用于删除节点的内容。

478

2023.09.01

JavaScript转义字符
JavaScript转义字符

JavaScript中的转义字符是反斜杠和引号,可以在字符串中表示特殊字符或改变字符的含义。本专题为大家提供转义字符相关的文章、下载、课程内容,供大家免费下载体验。

454

2023.09.04

js生成随机数的方法
js生成随机数的方法

js生成随机数的方法有:1、使用random函数生成0-1之间的随机数;2、使用random函数和特定范围来生成随机整数;3、使用random函数和round函数生成0-99之间的随机整数;4、使用random函数和其他函数生成更复杂的随机数;5、使用random函数和其他函数生成范围内的随机小数;6、使用random函数和其他函数生成范围内的随机整数或小数。

1031

2023.09.04

如何启用JavaScript
如何启用JavaScript

JavaScript启用方法有内联脚本、内部脚本、外部脚本和异步加载。详细介绍:1、内联脚本是将JavaScript代码直接嵌入到HTML标签中;2、内部脚本是将JavaScript代码放置在HTML文件的`<script>`标签中;3、外部脚本是将JavaScript代码放置在一个独立的文件;4、外部脚本是将JavaScript代码放置在一个独立的文件。

658

2023.09.12

Js中Symbol类详解
Js中Symbol类详解

javascript中的Symbol数据类型是一种基本数据类型,用于表示独一无二的值。Symbol的特点:1、独一无二,每个Symbol值都是唯一的,不会与其他任何值相等;2、不可变性,Symbol值一旦创建,就不能修改或者重新赋值;3、隐藏性,Symbol值不会被隐式转换为其他类型;4、无法枚举,Symbol值作为对象的属性名时,默认是不可枚举的。

553

2023.09.20

Java编译相关教程合集
Java编译相关教程合集

本专题整合了Java编译相关教程,阅读专题下面的文章了解更多详细内容。

9

2026.01.21

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Excel 教程
Excel 教程

共162课时 | 12.7万人学习

SciPy 教程
SciPy 教程

共10课时 | 1.2万人学习

PHP课程
PHP课程

共137课时 | 9万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号