元素码农
基础
UML建模
数据结构
算法
设计模式
网络
TCP/IP协议
HTTPS安全机制
WebSocket实时通信
数据库
sqlite
postgresql
clickhouse
后端
rust
go
java
php
mysql
redis
mongodb
etcd
nats
zincsearch
前端
浏览器
javascript
typescript
vue3
react
游戏
unity
unreal
C++
C#
Lua
App
android
ios
flutter
react-native
安全
Web安全
测试
软件测试
自动化测试 - Playwright
人工智能
Python
langChain
langGraph
运维
linux
docker
工具
git
svn
🌞
🌙
目录
▶
基础篇
▶
线性结构
数组实现原理
链表操作详解
双向链表详解
栈与队列应用
循环队列实现
▶
树形结构
二叉树遍历算法
堆结构实现
Trie树应用
AVL树原理
▶
散列结构
哈希表原理
哈希冲突解决
一致性哈希算法
▶
进阶篇
▶
图论结构
图的存储方式
最短路径算法
拓扑排序实现
▶
高级结构
跳表实现原理
并查集算法
布隆过滤器
R树索引结构
线段树应用
▶
数据库结构
B树与B+树
LSM树结构
红黑树应用
▶
实战应用
▶
性能优化
数据结构选型
内存布局优化
缓存友好设计
时间复杂度分析
空间复杂度优化
▶
工程实践
大规模数据处理
分布式数据结构
并发数据结构
数据结构测试方法
发布时间:
2025-03-21 15:54
↑
☰
# 图的存储方式 图是一种非线性数据结构,由顶点集合和边集合组成。在实际应用中,如何高效地存储和表示图结构是一个重要问题。本文将详细介绍几种常用的图存储方式及其实现。 ## 邻接矩阵 ### 基本概念 邻接矩阵是使用二维数组来表示图中顶点之间的连接关系。对于一个有n个顶点的图: - 矩阵大小为n×n - matrix[i][j] = 1表示顶点i和j之间有边 - matrix[i][j] = 0表示顶点i和j之间没有边 ### 实现示例 ```go // 邻接矩阵表示的图 type Graph struct { vertices int // 顶点数量 matrix [][]int // 邻接矩阵 } // 创建图 func NewGraph(v int) *Graph { // 初始化邻接矩阵 matrix := make([][]int, v) for i := range matrix { matrix[i] = make([]int, v) } return &Graph{ vertices: v, matrix: matrix, } } // 添加边 func (g *Graph) AddEdge(from, to int) { if from >= 0 && from < g.vertices && to >= 0 && to < g.vertices { g.matrix[from][to] = 1 g.matrix[to][from] = 1 // 无向图需要对称赋值 } } ``` ### 优缺点分析 优点: 1. 实现简单,直观 2. 查找两点间是否有边的时间复杂度为O(1) 3. 适合稠密图 缺点: 1. 空间复杂度为O(V²),V为顶点数 2. 对于稀疏图会浪费大量空间 3. 添加/删除顶点需要调整整个矩阵 ## 邻接表 ### 基本概念 邻接表使用数组+链表的方式存储图: - 数组的每个位置对应一个顶点 - 每个顶点维护一个链表,存储与其相邻的顶点 ### 实现示例 ```go // 邻接表节点 type Node struct { vertex int next *Node } // 邻接表表示的图 type AdjGraph struct { vertices int // 顶点数量 adjList []*Node // 邻接表 } // 创建图 func NewAdjGraph(v int) *AdjGraph { return &AdjGraph{ vertices: v, adjList: make([]*Node, v), } } // 添加边 func (g *AdjGraph) AddEdge(from, to int) { if from < 0 || from >= g.vertices || to < 0 || to >= g.vertices { return } // 添加from->to边 newNode := &Node{vertex: to} newNode.next = g.adjList[from] g.adjList[from] = newNode // 无向图需要添加to->from边 newNode = &Node{vertex: from} newNode.next = g.adjList[to] g.adjList[to] = newNode } ``` ### 优缺点分析 优点: 1. 空间效率高,尤其适合稀疏图 2. 容易找到顶点的所有邻接点 3. 添加边的操作简单高效 缺点: 1. 查找两点间是否有边需要遍历链表 2. 删除边的操作较复杂 ## 十字链表 ### 基本概念 十字链表是有向图的一种存储方式,它克服了邻接表表示有向图时出边和入边查找不方便的缺点。 ### 实现示例 ```go // 十字链表节点 type CrossNode struct { tailVex int // 弧尾顶点 headVex int // 弧头顶点 headLink *CrossNode // 指向下一个以headVex为弧头的弧 tailLink *CrossNode // 指向下一个以tailVex为弧尾的弧 } // 十字链表表示的图 type CrossGraph struct { vertices int // 顶点数量 vexList []CrossNode // 顶点表 } // 添加边 func (g *CrossGraph) AddEdge(from, to int) { node := &CrossNode{ tailVex: from, headVex: to, } // 将边节点插入headLink链表 node.headLink = g.vexList[to].headLink g.vexList[to].headLink = node // 将边节点插入tailLink链表 node.tailLink = g.vexList[from].tailLink g.vexList[from].tailLink = node } ``` ### 优缺点分析 优点: 1. 容易找到顶点的出边和入边 2. 适合有向图的存储和操作 3. 节省空间,适合稀疏图 缺点: 1. 结构较复杂 2. 需要额外的存储空间存储边的信息 ## 边集数组 ### 基本概念 边集数组是用一个数组存储图的所有边,每条边包含起点、终点和权重信息。 ### 实现示例 ```go // 边结构 type Edge struct { from int to int weight int } // 边集数组表示的图 type EdgeGraph struct { vertices int // 顶点数量 edges []Edge // 边集合 } // 添加边 func (g *EdgeGraph) AddEdge(from, to, weight int) { edge := Edge{ from: from, to: to, weight: weight, } g.edges = append(g.edges, edge) } // 获取所有边 func (g *EdgeGraph) GetAllEdges() []Edge { return g.edges } ``` ### 优缺点分析 优点: 1. 结构简单 2. 适合对边依次进行处理的操作 3. 适合Kruskal等以边为中心的算法 缺点: 1. 找到与某个顶点相关的边需要遍历整个数组 2. 不适合需要频繁查找顶点邻接关系的操作 ## 应用场景 ### 1. 邻接矩阵适用场景 - 图较小且稠密 - 需要快速判断两点间是否有边 - 图的动态性较小 ### 2. 邻接表适用场景 - 存储稀疏图 - 需要快速查找顶点的邻接点 - 经常需要添加新边 ### 3. 十字链表适用场景 - 有向图的存储 - 需要同时查找入边和出边 - 图比较稀疏 ### 4. 边集数组适用场景 - 需要对所有边进行排序 - 实现最小生成树算法 - 图的规模较小 ## 性能对比 | 操作 | 邻接矩阵 | 邻接表 | 十字链表 | 边集数组 | |------|---------|--------|----------|----------| | 存储空间 | O(V²) | O(V+E) | O(V+E) | O(E) | | 添加边 | O(1) | O(1) | O(1) | O(1) | | 删除边 | O(1) | O(degree) | O(degree) | O(E) | | 查找边 | O(1) | O(degree) | O(degree) | O(E) | | 遍历邻接点 | O(V) | O(degree) | O(degree) | O(E) | 注:V为顶点数,E为边数,degree为顶点的度 ## 实践建议 1. 选择合适的存储方式 - 根据图的稠密程度选择 - 考虑主要操作类型 - 权衡时间和空间效率 2. 注意实现细节 - 合理处理异常情况 - 考虑并发访问问题 - 注意内存管理 3. 优化建议 - 可以组合使用多种存储方式 - 针对特定应用场景优化 - 考虑缓存友好的实现 ## 总结 图的存储方式各有特点,需要根据具体应用场景选择合适的实现方式。在实际开发中,可以根据图的规模、稠密程度、操作特点等因素,选择最适合的存储方式,或者组合使用多种方式以达到最优的性能表现。