图数据结构核心解析:从邻接矩阵到邻接表的存储实战指南

发布时间:2026/8/1 21:07:14

图数据结构核心解析:从邻接矩阵到邻接表的存储实战指南 1. 从“关系”到“结构”为什么图是数据结构的终极形态干了这么多年开发从数组、链表到树总觉得数据结构的世界已经够用了。直到你遇到社交网络的好友推荐、地图App的路径规划或者微服务之间的调用链路分析才会发现之前学的那些“线”和“树”有点不够看了。它们能很好地表达一对一、一对多的关系但面对“多对多”这种复杂的网状关系时就显得力不从心。这时候“图”就登场了。你可以把图理解成描述“万物互联”的最基本、最强大的数学模型。它不再关心数据是不是排成一队线性或者有没有父子辈分层次它只关心两样东西实体和实体之间的关系。在图的术语里实体叫“顶点”或“节点”关系叫“边”。就这么简单却足以模拟现实世界中绝大多数复杂系统网页之间的超链接构成一张巨大的图城市和道路构成交通图人与人之间的社交关系构成社交图程序里的函数调用关系也能构成调用图。很多新手觉得图比树难其实不然。树是一种特殊的图无环连通图图是更一般、更通用的形式。理解图相当于拿到了解开复杂系统关系之谜的万能钥匙。今天我就结合自己踩过的坑和项目经验把图的定义、分类、术语和两种最核心的存储结构邻接矩阵和邻接表给你掰开揉碎了讲清楚。这不是教科书式的罗列而是一个老码农的实战笔记目标是让你看完就能懂懂了就能用。2. 图的定义与核心思想不止于点和线2.1 形式化定义一个三元组在数学和计算机科学里图G被严格定义为一个二元组或者更具体点一个三元组G (V, E, ψ)。别被符号吓到我用人话解释一下V (Vertex Set)顶点的有限非空集合。就是你研究的所有对象比如10个城市100个用户。E (Edge Set)边的有限集合。表示顶点之间的关系比如城市间的公路用户间的关注关系。ψ (Incidence Function)关联函数。它规定每条边具体连接的是哪两个或多个顶点。对于简单图这个函数通常隐含在边的定义中。举个例子我们要表示一个简单的社交网络有三位用户Alice(V1), Bob(V2), Charlie(V3)。如果Alice和Bob是好友Bob和Charlie也是好友那么这个图就是V {Alice, Bob, Charlie}E {好友关系1连接Alice和Bob 好友关系2连接Bob和Charlie}这个定义的核心思想是抽象。它剥离了城市、用户、网页这些具体概念的外衣只关注“点”和“线”的关系使得我们可以用同一套理论和方法去分析完全不同领域的问题。2.2 核心思想关系是第一性的与数组关注下标和值、链表关注前驱后继、树关注父子层级不同图数据结构将“关系”提升到了核心地位。在设计图相关的算法时你的思维模式需要转变从“这个数据是什么”转向“这个数据和哪些其他数据有关联”。这种思维在解决实际问题时威力巨大。比如在推荐系统中我们不再仅仅分析用户A的画像而是会去分析用户A所在的“关系子图”——他的好友喜欢什么、他关注的人买了什么这些关系边所传递的信息往往比顶点自身的属性更有价值。注意初学者常犯的一个错误是过于关注顶点本身的属性比如给城市顶点存储大量经济数据而忽略了边可能也承载着关键信息比如道路的长度、拥堵程度、关系亲密度。在设计图的数据结构时一定要预留边属性的存储空间。3. 图的分类认清你的战场图的世界很丰富不同类型的图对应不同的现实场景也决定了后续算法和存储结构的选择。主要可以从三个维度来分类。3.1 按边是否有方向有向图 vs. 无向图这是最基本也是最重要的分类。无向图边没有方向就像朋友关系。如果A是B的朋友那么B也一定是A的朋友。边(A, B)和(B, A)代表同一条边。社交网络中的好友关系、通信网络中的连接通常用无向图建模。有向图边有方向就像微博的关注关系。A关注B并不意味着B关注A。边(A, B)从A指向B和(B, A)是两条不同的边。网页的超链接、工作流的流程、函数调用链都是有向图的典型应用。选择依据如果你的关系中关系是相互的、对等的就用无向图如果关系是单向的、有因果的就用有向图。在代码中这直接影响存储结构。对于邻接矩阵无向图的矩阵是对称的对于邻接表有向图每个顶点的链表只存储“出边”或“入边”。3.2 按边是否有权重无权图 vs. 带权图无权图边只表示“有无连接”不量化连接的强度或成本。例如社交网络中的“是否认识”。带权图每条边都有一个相关的数值权重。这个权重可以代表距离、成本、时间、容量、相关性强度等。地图中道路的长度、网络中的带宽、交易图中的金额都需要用带权图表示。选择依据你的算法是否需要考虑关系的“度量”。最短路径算法Dijkstra必须用带权图而广度优先搜索BFS找最少中转次数用无权图即可。在存储时需要为边增加一个权重字段。3.3 按图的复杂程度简单图 vs. 复杂图简单图满足两个条件1) 任意两个顶点之间最多有一条边无重边2) 没有顶点到自身的边无自环。大多数理论讨论和基础算法都基于简单图。非简单图复杂图多重图允许两个顶点间有多条平行的边。比如两个城市之间有多条不同航班号的道路。自环图允许顶点有连接自身的边。这在某些电路图或状态机中会出现。超图一条边可以连接两个以上的顶点。比如一篇论文边有多个作者顶点。选择依据现实世界的数据往往不是“简单”的。在建模时首先要判断你的场景是否存在重边或自环。如果存在选择存储结构时比如邻接矩阵就需要调整因为标准邻接矩阵无法直接表示多重边。3.4 按边的密度稠密图 vs. 稀疏图这是一个非常实用的分类直接决定了你应该选择哪种存储结构从而影响算法的效率。稠密图边数|E|接近顶点数|V|的平方即|E| ≈ |V|²。顶点之间几乎两两相连。例如一个地区所有机场之间的直飞航线图如果航线很多。稀疏图边数远小于顶点数的平方即|E| |V|²。顶点之间连接稀少。例如全国的公路网每个城市只与邻近几个城市相连社交网络一个人通常只与几百人有关联而网络有数十亿人。经验法则这是一个定性判断。通常如果|E|是|V|的常数倍如10|V|那就是稀疏图如果接近|V|²就是稠密图。稀疏图用邻接表稠密图用邻接矩阵这是优化性能的黄金准则。4. 图的术语详解沟通的共同语言理解了分类我们还需要一套精确的“行话”来描述图中的细节。这些术语是阅读算法文献和与人交流的基础。4.1 顶点与边的基本关系邻接如果一条边e连接了顶点u和v则称u和v是相邻的边e与顶点u和v是相关联的。度无向图中顶点的度与该顶点相关联的边的条数。记作deg(v)。例如一个顶点有3条边连接它的度就是3。有向图中顶点的度入度以该顶点为终点的边的数目。表示有多少条边“指向”它。出度以该顶点为起点的边的数目。表示它“指向”多少其他顶点。总度入度与出度之和。路径与回路路径一个顶点序列v1, v2, ..., vk使得对于i1,2,...,k-1(vi, vi1)都是图中的边。路径的长度是经过的边数无权图或是边权重之和带权图。简单路径路径中所有顶点互不相同除了起点终点可能相同。回路环起点和终点相同的路径。如果该路径是简单路径则称为简单回路。4.2 图的连通性连通图无向图图中任意两个顶点之间都存在路径。整个图是一个整体。连通分量无向图的一个极大连通子图。一个不连通的无向图由多个连通分量组成。例如一个社交网络中可能有一个大群体和几个孤立的小群体每个群体就是一个连通分量。强连通图有向图图中任意两个顶点u和v之间既存在从u到v的路径也存在从v到u的路径。强连通分量有向图的极大强连通子图。有向图的连通性分析更复杂常用Kosaraju或Tarjan算法来寻找强连通分量。4.3 特殊形态的图完全图无向图中任意两个不同的顶点之间都恰有一条边。n个顶点的无向完全图记作Kn其边数为n(n-1)/2。这是最稠密的图。有向无环图没有环的有向图。这是图论中极其重要的一类图是任务调度、依赖管理如Makefile、包管理、版本历史的核心模型。拓扑排序是其标志性算法。树与森林无环连通无向图就是树。多个互不相连的树构成森林。树是图的特例也是最简单的图。实操心得在调试图算法时打印出每个顶点的度、图的连通分量数量、是否存在环这些基本信息能帮你快速判断数据加载是否正确、算法在哪个环节出了问题。把这些基础检查写成工具函数能节省大量调试时间。5. 图的存储结构一邻接矩阵——直观的“表格法”存储结构的目标是把图G(V,E)这个数学概念塞进计算机的内存里。邻接矩阵是最直观的一种方法。5.1 原理与结构它的思想很简单用一个n x n的二维数组矩阵matrix来表示一个n个顶点的图。如果顶点i到顶点j有一条边那么matrix[i][j] 1无权图或 weight带权图。如果没有边则matrix[i][j] 0或一个特殊值如INF表示无穷大。对于无向图由于边是双向的矩阵会是一个对称矩阵即matrix[i][j] matrix[j][i]。假设我们有一个4个顶点的无向无权图边为(1,2), (1,3), (2,4), (3,4)。其邻接矩阵如下通常顶点编号从0或1开始这里从1开始便于理解1234101102100131001401105.2 代码实现示例C#include iostream #include vector using namespace std; class GraphWithMatrix { private: int numVertices; vectorvectorint adjMatrix; // 二维动态数组 bool isDirected; public: // 构造函数初始化n x n的矩阵所有元素为0 GraphWithMatrix(int n, bool directed false) : numVertices(n), isDirected(directed) { adjMatrix.resize(n, vectorint(n, 0)); } // 添加边无权图 void addEdge(int u, int v) { // 假设顶点编号从0开始 if (u 0 u numVertices v 0 v numVertices) { adjMatrix[u][v] 1; if (!isDirected) { // 如果是无向图对称位置也设为1 adjMatrix[v][u] 1; } } } // 添加带权边 void addEdge(int u, int v, int weight) { if (u 0 u numVertices v 0 v numVertices) { adjMatrix[u][v] weight; if (!isDirected) { adjMatrix[v][u] weight; } } } // 打印邻接矩阵 void printMatrix() { for (int i 0; i numVertices; i) { for (int j 0; j numVertices; j) { cout adjMatrix[i][j] ; } cout endl; } } // 判断两个顶点是否相邻O(1)时间复杂度 bool isAdjacent(int u, int v) { if (u 0 u numVertices v 0 v numVertices) { return adjMatrix[u][v] ! 0; } return false; } }; int main() { // 创建一个4个顶点、无向的图 GraphWithMatrix g(4, false); g.addEdge(0, 1); // 对应顶点1-2 g.addEdge(0, 2); // 对应顶点1-3 g.addEdge(1, 3); // 对应顶点2-4 g.addEdge(2, 3); // 对应顶点3-4 cout Adjacency Matrix: endl; g.printMatrix(); // 输出 // 0 1 1 0 // 1 0 0 1 // 1 0 0 1 // 0 1 1 0 cout Is vertex 0 adjacent to vertex 2? (g.isAdjacent(0, 2) ? Yes : No) endl; // Yes return 0; }5.3 邻接矩阵的优缺点与适用场景优点直观易懂矩阵形式非常符合人类阅读习惯图的整体结构一目了然。操作高效查询边是否存在O(1)时间复杂度直接访问matrix[i][j]即可。这是它最大的优势。添加或删除边同样也是O(1)。适合稠密图当边数接近n²时矩阵的空间利用率高且常数时间的边查询优势得以充分发挥。便于数学运算矩阵可以与许多数学理论和算法结合例如通过计算矩阵的幂来求两点间长度为k的路径数。缺点空间复杂度高O(n²)。对于顶点数很多例如10万的稀疏图即使只有几十万条边也需要开辟100亿的存储单元其中绝大部分是0造成巨大的内存浪费。这是其致命伤。遍历邻接点效率低要找出顶点v的所有邻居必须扫描矩阵的第v行或列时间复杂度为O(n)。即使它只有3个邻居你也得扫描完n个元素。动态增删顶点困难改变矩阵大小需要重新分配和复制整个二维数组成本很高。适用场景总结图规模较小顶点数n在几千以内。图是稠密图或需要频繁判断任意两点间是否有边。需要进行图论相关的矩阵运算。作为学习理解图概念的入门工具。踩坑记录在早期的一个网络拓扑分析项目中我贸然对一个有5000个节点、约8000条边典型的稀疏图的数据使用了邻接矩阵。结果程序刚启动就吃掉了近200MB内存500050008字节假设用double而且每次找邻居的循环都慢得惊人。后来换成邻接表内存降到1MB以内遍历速度提升了几十倍。这个教训让我深刻理解了“稀疏图不用邻接矩阵”这条铁律。6. 图的存储结构二邻接表——灵活的“链表法”为了解决邻接矩阵在稀疏图上的空间浪费问题邻接表应运而生。它的核心思想是只为实际存在的边分配存储空间。6.1 原理与结构邻接表的结构是一个“数组链表”的组合也可以用动态数组如vector代替链表用一个大小为n的数组或vector来表示所有顶点。数组的每个元素对应一个顶点。每个数组元素本身是一个容器链表、动态数组等用于存储与该顶点直接相邻的所有顶点对于有向图通常存储出边邻居。对于同一个无向图顶点1,2,3,4边(1,2), (1,3), (2,4), (3,4)其邻接表结构如下顶点1 - [2] - [3] 顶点2 - [1] - [4] 顶点3 - [1] - [4] 顶点4 - [2] - [3]可以看到每条边(u, v)在邻接表中存储了两次分别在u和v的链表里因为无向图中边是双向的关系。6.2 代码实现示例C使用vector#include iostream #include vector using namespace std; // 定义边的结构体用于带权图 struct Edge { int destVertex; // 目标顶点 int weight; // 边权重 Edge(int v, int w) : destVertex(v), weight(w) {} }; class GraphWithAdjList { private: int numVertices; bool isDirected; // 使用 vector 的 vector 来存储邻接表。每个内层vector存储该顶点的所有邻居。 // 对于无权图内层vector存储int邻居顶点编号即可。 // 对于带权图内层vector存储Edge结构体。 vectorvectorEdge adjList; public: GraphWithAdjList(int n, bool directed false) : numVertices(n), isDirected(directed) { adjList.resize(n); } // 添加带权边 void addEdge(int u, int v, int weight 1) { if (u 0 u numVertices v 0 v numVertices) { adjList[u].push_back(Edge(v, weight)); if (!isDirected) { // 无向图需要添加反向边 adjList[v].push_back(Edge(u, weight)); } } } // 打印邻接表 void printAdjList() { for (int i 0; i numVertices; i) { cout Vertex i - ; for (const Edge edge : adjList[i]) { cout ( edge.destVertex , w: edge.weight ) ; } cout endl; } } // 获取顶点v的所有邻居出边 const vectorEdge getNeighbors(int v) { if (v 0 v numVertices) { return adjList[v]; } static vectorEdge emptyVector; // 返回空引用避免拷贝 return emptyVector; } // 判断边是否存在时间复杂度O(deg(v)) bool isAdjacent(int u, int v) { if (u 0 u numVertices) { for (const Edge edge : adjList[u]) { if (edge.destVertex v) { return true; } } } return false; } }; int main() { GraphWithAdjList g(4, false); g.addEdge(0, 1, 5); // 边(0,1)权重5 g.addEdge(0, 2, 3); g.addEdge(1, 3, 2); g.addEdge(2, 3, 7); cout Adjacency List: endl; g.printAdjList(); // 输出示例 // Vertex 0 - (1, w:5) (2, w:3) // Vertex 1 - (0, w:5) (3, w:2) // Vertex 2 - (0, w:3) (3, w:7) // Vertex 3 - (1, w:2) (2, w:7) // 遍历顶点0的所有邻居 cout Neighbors of vertex 0: ; for (const Edge e : g.getNeighbors(0)) { cout e.destVertex ; } cout endl; // 输出: 1 2 return 0; }6.3 邻接表的优缺点与适用场景优点空间效率高空间复杂度为O(|V| |E|)。对于稀疏图这比邻接矩阵的O(|V|²)节省了巨额内存。这是它最核心的优势。遍历邻接点高效列出顶点v的所有邻居时间复杂度为O(deg(v))即与其度数成正比。对于度数很低的顶点速度极快。易于动态增删边在链表或vector末尾添加或删除元素边是高效的。天然支持存储边属性链表节点或vector元素可以轻松扩展为结构体存储权重、类型等附加信息。缺点查询特定边效率低判断边(u, v)是否存在需要遍历u的邻接链表时间复杂度为O(deg(u))最坏情况O(n)。不如邻接矩阵的O(1)。实现稍复杂需要管理链表或动态数组代码比邻接矩阵略复杂。对重边处理如果需要区分平行边多重图邻接表可以存储多次但查询某条特定边时会变得更麻烦。适用场景总结绝大多数情况尤其是稀疏图。现实世界中的图社交网络、交通网络、知识图谱几乎都是稀疏图因此邻接表是事实上的标准选择。需要频繁遍历顶点邻居的算法如广度优先搜索、深度优先搜索、Dijkstra算法等。内存受限或图的顶点规模非常大的场景。进阶技巧邻接表的变体使用vector代替链表在现代C中使用vectorvectorEdge通常比list或手写链表性能更好因为内存连续缓存友好。除非需要频繁在中间插入/删除边否则vector是首选。链式前向星这是一种用数组模拟链表的静态邻接表常见于算法竞赛。它将所有边存储在一个大数组中用next指针索引极致节省内存且访问速度快但不支持动态增删边。邻接集set或unordered_set如果需要快速判断边是否存在且不关心邻居顺序可以用哈希集合存储邻居。这样isAdjacent操作可以优化到接近O(1)但遍历邻居和空间开销会略大。7. 邻接矩阵与邻接表的综合对比与选型指南光知道优缺点还不够到底该怎么选我总结了一个决策流程和对比表格。决策流程评估图密度粗略估算|E|与|V|²的关系。如果|E|接近|V|²倾向于矩阵如果|E|远小于|V|²比如|E| 10|V|坚决用邻接表。明确核心操作如果你的算法核心是频繁查询“任意两点间是否有边”比如某些图论证明或特定算法邻接矩阵的O(1)查询是无可替代的。如果你的算法核心是遍历BFS/DFS或需要频繁访问某个点的所有邻居比如社交网络的好友遍历、路径搜索邻接表的O(deg(v))遍历效率远高于矩阵的O(n)。考虑内存约束对于顶点数上万的大型图先算一笔内存账。假设顶点数n10000使用int型矩阵需要10000*10000*4 bytes ≈ 400MB。而同样规模的稀疏图假设平均度数10邻接表只需(10000 10*10000) * 4 bytes ≈ 0.44MB这里简化估算实际链表有开销。差距上千倍。动态性要求如果图结构顶点和边需要频繁动态增删邻接表更灵活。对比表格特性邻接矩阵邻接表空间复杂度O(V查询边 (u, v)O(1)O(deg(u))最坏O(遍历顶点v的所有邻居O(V添加边O(1)O(1)(平均在vector末尾添加)删除边O(1)O(deg(u))(需要查找)添加顶点O(V适合图类型稠密图小规模图稀疏图大规模图优点实现简单查边快适合矩阵运算空间省遍历邻居快动态性好缺点空间浪费大增删顶点慢查边慢实现稍复杂个人经验之谈在超过95%的工程实践中你都会使用邻接表。除非你非常确定图很小且很稠密或者有极强的O(1)查边需求否则无脑选邻接表或其变体基本不会错。很多高级图数据库和计算框架如NetworkX, Neo4j, Spark GraphX底层也都是基于邻接表的思想进行优化和扩展的。8. 常见问题与实战排查技巧理论懂了代码写了真正用起来还是会遇到各种问题。下面是我在项目和面试中总结的几个高频问题和解决思路。8.1 如何选择顶点编号的起始索引这是一个看似简单却容易引发“off-by-one”错误的问题。从0开始符合C/C、Java、Python等绝大多数编程语言数组的惯例。代码更自然不易出错。强烈推荐。从1开始有时输入数据或问题描述中顶点编号从1开始。处理策略内部统一从0开始无论输入如何在读取顶点编号后立即执行u--, v--转换为0基索引。所有内部存储和计算都基于0。输出时转换回原编号在需要输出结果如打印路径时再执行u1, v1转换回去。数组大小如果顶点编号是1到n声明数组时大小应为n1并忽略下标0的位置或将其用作哨兵。这种方法容易浪费一个空间且思维需要转换不推荐。// 推荐做法内部统一使用0基 int n; // 顶点数编号1..n cin n; Graph g(n); // 图类内部按n个顶点分配空间 for (int i 0; i m; i) { // m条边 int u, v; cin u v; u--; v--; // 转换为0基索引 g.addEdge(u, v); } // ... 执行算法 ... // 输出时如果需要1基编号 cout Path: ; for (int vertex : path) { cout vertex 1 ; }8.2 处理带权图时如何表示“无穷大”在最短路径算法如Floyd, Dijkstra中需要用一个值表示“不连通”或“距离无穷大”。选择原则这个值必须大于任何可能出现的实际路径权重之和。常用方法使用一个非常大的数如0x3f3f3f3f。这个数约等于10^9在通常的题目和场景中足够大而且两个它相加也不会溢出32位int范围0x3f3f3f3f * 2 INT_MAX这是一个常用技巧。使用特定数据类型的最大值如INT_MAX/DBL_MAX。但要小心在做加法时溢出例如INT_MAX 1会变成负数。使用这种方法时在松弛操作中需要先判断是否为“无穷大”再相加。初始化在邻接矩阵中将不存在的边初始化为INF在邻接表中不存在的边自然不会出现在链表里但在距离数组dist[]中需要初始化为INF。const int INF 0x3f3f3f3f; // 邻接矩阵初始化 vectorvectorint graph(n, vectorint(n, INF)); for (int i 0; i n; i) graph[i][i] 0; // 自己到自己的距离为0 // 距离数组初始化 vectorint dist(n, INF); dist[start] 0;8.3 邻接表遍历时如何避免修改原始图结构在遍历邻接表特别是使用引用时如果你在遍历过程中意外地添加或删除了边可能会使迭代器失效导致程序崩溃或未定义行为。安全遍历模式// 假设 adjList 是 vectorvectorint for (int i 0; i adjList.size(); i) { // 如果需要遍历顶点i的所有邻居 for (int neighbor : adjList[i]) { // 使用值拷贝安全 // 对 neighbor 进行操作 } // 或者使用常量引用但确保循环内不修改 adjList[i] // for (const int neighbor : adjList[i]) { ... } } // **危险操作**在遍历过程中添加边 // for (int neighbor : adjList[u]) { // if (someCondition) { // adjList[u].push_back(newVertex); // 这可能导致vector重新分配内存迭代器失效 // } // }如果必须在遍历过程中修改图结构一个安全的做法是先收集需要进行的操作遍历结束后再执行。例如先记录要添加的边到一个临时列表循环结束后再统一添加。8.4 如何为顶点和边添加丰富的属性基础的邻接表只存储顶点编号和边权重。现实应用中顶点可能有名称、类型、坐标边可能有类型、创建时间、可信度等。设计模式属性与结构分离这是最清晰的方式。用单独的数组或映射来存储属性用顶点ID作为键。class Graph { private: vectorvectorEdge adjList; // 只存目标顶点和边权重 vectorstring vertexNames; // vertexNames[i] 存储顶点i的名称 vectorPoint vertexCoords; // vertexCoords[i] 存储顶点i的坐标 // 边属性可能更复杂可以用一个从边ID到属性的map边ID可以用(u,v)的哈希值生成 };使用结构体封装将邻接表中的元素从简单的int或pair升级为结构体。struct Vertex { int id; string name; double x, y; vectorEdge outgoingEdges; }; struct Edge { int destVertexId; double weight; string relationshipType; int timestamp; }; vectorVertex graph;选择哪种方式取决于你的访问模式。如果频繁需要根据顶点ID获取其所有属性第二种更紧凑如果属性访问不频繁第一种更灵活内存也可能更优因为属性数组可以按需加载。8.5 调试图算法时有哪些可视化或检查技巧图结构不直观调试不能只靠cout。打印小规模图实现一个printGraph()函数以邻接表或矩阵形式打印出来人工核对。单元测试用极小的、已知结果的图如3-5个顶点测试你的算法。例如一个三角形图的最短路径应该很容易心算验证。可视化工具进阶对于复杂图可以输出为DOT语言格式然后用 Graphviz 工具生成图片。void outputToDot(const Graph g, const string filename) { ofstream fout(filename); fout graph G { endl; for (int i 0; i g.numVertices(); i) { for (const Edge e : g.getNeighbors(i)) { if (i e.destVertex) { // 无向图避免重复输出边 fout i -- e.destVertex; if (e.weight ! 1) fout [label\ e.weight \]; fout ; endl; } } } fout } endl; fout.close(); // 然后在命令行执行dot -Tpng output.dot -o output.png }检查图的基本性质编写辅助函数检查图是否连通通过一次DFS/BFS能访问的顶点数是否等于总顶点数、是否有环通过DFS检查回边、所有顶点的度之和是否等于边数的两倍无向图等。这些基本检查能快速发现数据加载或构建的错误。掌握图的存储结构就像拿到了建造图算法大厦的砖瓦。邻接矩阵规整但笨重邻接表灵活而高效。理解它们各自的脾性根据你的数据规模和算法需求做出明智选择是迈向图算法实战的第一步。接下来当你开始探索深度优先搜索、最短路径、最小生成树这些经典算法时你会庆幸自己在这里打好了坚实的基础。毕竟再精妙的算法也需要一个合适的数据结构来承载。

相关新闻