跳到主要内容数据结构:图论基础 | 极客日志C++算法
数据结构:图论基础
图论的基础概念,包括顶点、边、有向图与无向图等基本定义。阐述了图的类型(稀疏、稠密、加权等)及表示方法(邻接矩阵、邻接表)。提供了基于 C++ 的邻接矩阵和邻接表实现代码示例,涵盖初始化、添加边等核心接口。最后总结了图论的应用场景及后续学习方向。
RustyLab2 浏览 图的概念
图(Graph)是离散数学中的一种重要数据结构,用于描述对象(称为顶点,或节点)之间的关系(称为边)。图可以表示各种事物之间的连接关系,比如网络中的路由器、社交网络中的用户、城市之间的道路等。
图的基本概念
- 顶点(Vertex):
- 图中的基本单位,也称为节点(Node)。一个图通常由若干个顶点组成,用来表示实体或对象。
- 边(Edge):
- 顶点之间的连接关系称为边。边可以是有方向的(有向图)或无方向的(无向图)。在无向图中,边表示双向关系;在有向图中,边表示单向关系。
- 有向图(Directed Graph, Digraph):
- 边具有方向性,表示从一个顶点指向另一个顶点的单向连接。
- 无向图(Undirected Graph):
- 边没有方向,表示两个顶点之间的对称关系,如'相邻'或'连接'。
- 邻接(Adjacency):
- 如果两个顶点之间有一条边连接,这两个顶点称为邻接的。
- 度(Degree):
- 一个顶点的度是连接到该顶点的边的数目。在有向图中,区分入度(In-degree)和出度(Out-degree),分别表示进入该顶点和从该顶点发出的边的数量。
图的类型
- 稀疏图(Sparse Graph):
- 边的数量远远少于顶点数量的平方(E << V²),大部分顶点之间没有边连接。
- 稠密图(Dense Graph):
- 边的数量接近顶点数量的平方(E ≈ V²),大多数顶点之间有边连接。
- 加权图(Weighted Graph):
- 图中的每条边带有一个权重,用来表示顶点之间的某种度量,如距离、成本或容量。
- 连通图(Connected Graph):
- 对于无向图,若任意两个顶点之间都有路径相连,则称为连通图。有向图中则需区分强连通和弱连通。
- 简单图(Simple Graph):
- 不包含自环和多重边的图。自环是指从顶点到自身的边,多重边是指两个顶点之间存在多条边。
- 完全图(Complete Graph):
- 图中任意两个顶点之间都有边相连,通常表示为 Kn,其中 n 是顶点数。
图的表示方法
- 邻接矩阵(Adjacency Matrix):
- 使用一个二维矩阵来表示顶点之间的连接关系,矩阵中的元素表示边的存在性或权重。
- 邻接表(Adjacency List):
- 对每个顶点,维护一个链表(或数组)来存储与之相邻的顶点列表,适合稀疏图。
- 边集列表(Edge List):
- 直接列出图中所有的边,用边的起点和终点来描述,适合图的遍历或算法中的具体操作。
图的相关基本概念
在理解图的结构和操作时,有一些与图密切相关的概念有助于更好地分析和处理图的算法和应用。
1. 路径(Path)
路径是在图中从一个顶点到另一个顶点的行进序列,它由一系列边和顶点组成。根据图的类型和要求,路径可以分为几类:
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- 简单路径(Simple Path):路径中的顶点不重复出现。
- 回路(Cycle):路径的起点和终点是同一个顶点,且路径中的其他顶点不重复。
2. 连通性(Connectivity)
- 连通图(Connected Graph):对于无向图,如果从任意一个顶点能到达其他所有顶点,则图是连通的。
- 强连通图(Strongly Connected Graph):对于有向图,如果任意两个顶点之间都有路径相通,则图是强连通的。
- 弱连通图(Weakly Connected Graph):对于有向图,如果将其所有边看作无向边,能够使整个图连通,则图是弱连通的。
3. 图的度(Degree)
- 入度(In-degree):有向图中指向该顶点的边的数量。
- 出度(Out-degree):有向图中从该顶点发出的边的数量。
4. 子图(Subgraph)
子图是从原始图中选取部分顶点和边构成的图。常见的子图类型包括:
- 生成子图(Spanning Subgraph):包含图的所有顶点,但边可能有所删减。
- 诱导子图(Induced Subgraph):包含图中某些顶点及这些顶点之间的所有边。
5. 生成树(Spanning Tree)
生成树是无向图的一个子图,包含图中的所有顶点,且是一个树形结构(无环、连通)。
- 最小生成树(Minimum Spanning Tree, MST):在加权图中,生成树的边权和最小的生成树。
6. 二分图(Bipartite Graph)
二分图是一种特殊的图,可以将顶点集合分为两个不相交的子集,且所有边都连接两个不同子集中的顶点,而子集中没有内部连接。
7. 拓扑排序(Topological Sort)
对于有向无环图(DAG),拓扑排序是一种将顶点按依赖关系线性排序的算法,通常用于解决调度、依赖管理等问题。
8. 欧拉图和哈密顿图
- 欧拉路径(Eulerian Path):经过图中每条边一次且仅一次的路径。如果这样的路径是闭合的,即路径的起点和终点相同,则称为欧拉回路(Eulerian Circuit)。
- 哈密顿路径(Hamiltonian Path):经过图中每个顶点一次且仅一次的路径。如果这样的路径是闭合的,则称为哈密顿回路(Hamiltonian Circuit)。
实现图
邻接矩阵实现图
如果是无向图的话,那么邻接矩阵就是沿着对角线对称的。
如果是无向图的话,那么就可以通过压缩只存储一半。
除了需要一个存储权值的邻接矩阵我们还需要一个 vector 来存储顶点,如果涉及到邻接矩阵,那么就会涉及到下标,所以我们应该还需要一个顶点映射下标的 map。
邻接矩阵的基本框架
template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
class Graph {
public:
private:
vector<V> _vertexs;
map<V, int> _indexMap;
vector<vector<W>> _matrix;
};
核心接口
Graph(const V* a, size_t n) {
_vertexs.resize(n);
for (size_t i = 0; i < n; i++) {
_vertexs[i] = a[i];
_indexMap[a[i]] = i;
}
_matrix.resize(n);
for (size_t i = 0; i < n; i++)
_matrix[i].resize(n, MAX_W);
}
size_t GetVertexIndex(const V& v) {
auto it = _indexMap.find(v);
if (it != _indexMap.end())
return it->second;
else {
throw invalid_argument("顶点不存在");
return -1;
}
}
通过 map 的接口 find 找到 v 对应的下标,如果 it 为 end(),说明没有这个顶点,直接抛异常,如果存在,则返回对应的下标。
void AddEdge(const V& src, const V& dst, const W& w) {
size_t srci = GetVertexIndex(src);
size_t dsti = GetVertexIndex(dst);
_matrix[srci][dsti] = w;
if (Direction == false)
_matrix[dsti][srci] = w;
}
找到原点和目标点对应的下标,如果是有向图的话,只需要添加原点到目标点的边,如果是无向图的话,就需要反向添加一下目标点到原点的边了。
邻接表实现图
邻接表是图的一种存储表示方法,常用于存储稀疏图(即边数相对较少的图)。它通过每个顶点对应一个链表或数组来记录与其相邻的顶点。邻接表的空间复杂度相对较小,适合存储大规模图。
对于无向图来说不存在入度和出度的概念,所以只需要一个邻接表表示,下标的邻接表就是用来表示边的集合。
拿第一个点为例,这里使用一个结构体来维护的,这个结构体中有指向一下下一个边集的指针,还有一个权值和目标点的下标。
类似于哈希桶的那个做法。
邻接表实现图的基本框架
template<class W>
struct Edge {
int _dsti;
W _w;
Edge<W>* _next;
Edge(int dsti, const W& w) : _dsti(dsti), _w(w), _next(nullptr) {}
};
template<class V, class W, bool Direction = false>
class Graph {
typedef Edge<W> Edge;
public:
private:
vector<V> _vertexs;
map<V, int> _indexMap;
vector<Edge*> _tables;
};
核心接口
Graph(const V* a, size_t n) {
_vertexs.resize(n);
for (size_t i = 0; i < n; i++) {
_vertexs[i] = a[i];
_indexMap[a[i]] = i;
}
_tables.resize(n, nullptr);
}
size_t GetVertexIndex(const V& v) {
auto it = _indexMap.find(v);
if (it != _indexMap.end())
return it->second;
else {
throw invalid_argument("顶点不存在");
return -1;
}
}
void AddEdge(const V& src, const V& dst, const W& w) {
size_t srci = GetVertexIndex(src);
size_t dsti = GetVertexIndex(dst);
Edge* eg = new Edge(dsti, w);
eg->_next = _tables[srci];
_tables[srci] = eg;
if (Direction == false) {
Edge* eg = new Edge(srci, w);
eg->_next = _tables[dsti];
_tables[dsti] = eg;
}
}
这个和邻接矩阵就不一样,邻接表中添加边应该先创建一个对应边的结构体,然后将这个结构体头插到原点对应下标的边集的位置上,如果是无向图的话,需要反向添加一条目标点到原点的边。注意:头插之后需要改变头的位置。
总结
通过本篇文章的介绍,我们初步了解了图的基本概念、图的表示方法(如邻接矩阵和邻接表)、以及图中的各种基本性质。图论作为计算机科学和数学中的一个重要分支,其应用范围广泛,从网络设计到路径规划,都有着广泛的应用场景。
在接下来的学习中,我们可以进一步探讨更高级的图算法,如最短路径算法(Dijkstra、Bellman-Ford 等)、图的遍历算法(深度优先搜索、广度优先搜索)、以及图的连通性和最小生成树等高级主题。这些内容将为解决更复杂的实际问题提供坚实的理论基础。