当前位置:首页 > 文章列表 > 文章 > 前端 > 图结构的两种表示方法:邻接表与邻接矩阵

图结构的两种表示方法:邻接表与邻接矩阵

2025-08-14 16:09:31 0浏览 收藏

图是一种强大的非线性数据结构,用于表示对象间的多对多关系,广泛应用于社交网络、导航和推荐系统等领域。本文深入探讨了图的定义、特点及其在JavaScript中的实现方式,重点介绍了**邻接表**和**邻接矩阵**两种主流表示方法。邻接表通过对象或数组存储顶点的相邻顶点,尤其适合稀疏图和动态操作;而邻接矩阵则使用二维数组表示顶点间的连接关系,更适用于稠密图。文章还提供了使用JavaScript实现图结构的示例代码,包括添加顶点、边,以及广度优先搜索(BFS)和深度优先搜索(DFS)等关键操作,助你轻松掌握图结构在JS中的应用。选择合适的图表示方法,优化你的数据结构,提升程序性能!

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

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

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

邻接表的优势在于:

  • 空间效率高: 对于稀疏图(边数远小于顶点数的平方),它只存储实际存在的边,所以占用的内存更少。想一下,如果一个图有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)是实现这两种遍历的核心机制。实际项目里,你可能还会遇到带权图(边有权重)、有向图(边有方向)、多重图(两点间有多条边)等更复杂的场景,但核心的表示和遍历思想是相通的,只需在边的存储和遍历逻辑上做相应调整。

今天带大家了解了的相关知识,希望对你有所帮助;关于文章的技术知识我们会一点点深入介绍,欢迎大家关注golang学习网公众号,一起学习编程~

CSS骨架屏加载动画技巧CSS骨架屏加载动画技巧
上一篇
CSS骨架屏加载动画技巧
HTMLtextarea自适应内容高度的4种方法
下一篇
HTMLtextarea自适应内容高度的4种方法
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    542次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    511次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    498次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    484次学习
查看更多
AI推荐
  • 千音漫语:智能声音创作助手,AI配音、音视频翻译一站搞定!
    千音漫语
    千音漫语,北京熠声科技倾力打造的智能声音创作助手,提供AI配音、音视频翻译、语音识别、声音克隆等强大功能,助力有声书制作、视频创作、教育培训等领域,官网:https://qianyin123.com
    168次使用
  • MiniWork:智能高效AI工具平台,一站式工作学习效率解决方案
    MiniWork
    MiniWork是一款智能高效的AI工具平台,专为提升工作与学习效率而设计。整合文本处理、图像生成、营销策划及运营管理等多元AI工具,提供精准智能解决方案,让复杂工作简单高效。
    165次使用
  • NoCode (nocode.cn):零代码构建应用、网站、管理系统,降低开发门槛
    NoCode
    NoCode (nocode.cn)是领先的无代码开发平台,通过拖放、AI对话等简单操作,助您快速创建各类应用、网站与管理系统。无需编程知识,轻松实现个人生活、商业经营、企业管理多场景需求,大幅降低开发门槛,高效低成本。
    170次使用
  • 达医智影:阿里巴巴达摩院医疗AI影像早筛平台,CT一扫多筛癌症急慢病
    达医智影
    达医智影,阿里巴巴达摩院医疗AI创新力作。全球率先利用平扫CT实现“一扫多筛”,仅一次CT扫描即可高效识别多种癌症、急症及慢病,为疾病早期发现提供智能、精准的AI影像早筛解决方案。
    172次使用
  • 智慧芽Eureka:更懂技术创新的AI Agent平台,助力研发效率飞跃
    智慧芽Eureka
    智慧芽Eureka,专为技术创新打造的AI Agent平台。深度理解专利、研发、生物医药、材料、科创等复杂场景,通过专家级AI Agent精准执行任务,智能化工作流解放70%生产力,让您专注核心创新。
    186次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码