一、介绍
在前面的很多文章中,其实都对图或多或少的进行过分析说明。比如内存的垃圾回收系列、多线程系列以及最新的并行系统中的CUDA和TBB相关的运行图等。
之所以没有认真的分析图相关的数据结构和算法的原因,一个是因为图相对复杂,应用相对专业;另外一个是就是对于大多数的开发者来说,实际遇到图的情况也不多。更多的情况是即使遇到,也大多是简单应用。
本文仍然不会深入展开对图的相关算法和数据结构的详细分析,但会做一个相对整体的总结说明,并对相关的应用进行分析。
二、图
要想谈图,就先得把图的数学定义搞清楚,这样就会有一个严格的限制不会导致表述不清楚引发的各种问题。比如在离散数学中,图的二元组定义为:
一个图𝐺定义为一个有序对G=(V,E),其中:
- V是一个非空集合,称为顶点集(Vertex Set),其元素称为顶点或节点
- E 是一个边集(Edge Set),其元素称为边
- 边与顶点之间存在关联关系:每条边都有它的端点(即连接哪些顶点)。
需要说明的是,这里不是在讨论严格的数学定义,也不对图进行各种方法的展开。即图的三元组定义方式可以参看相关的数学书籍和内容。
从上面的定义可以看出,图其实就是边和顶点的集合。但在图中有几种特殊情况需要注意,一个是环,即在图中形成了一个循环,它包括自身循环和回路;另外一个就是顶点间存在着多个边(多重边)即多重图。


其中环开发者还是经常可以遇到的,但多重图就接触的很少了。
三、分类
图在实践中的分类非常灵活,常见的分类方式包括以下几类:
- 按边方向分类
这是最常见的一种情况,即边有方向则为有向图(Directed Graph);无方向则为无向图(Undirected Graph) - 按权重分类
这种也比较常见,即按边的关系的权重值为划分。可分为无权图(Unweighted Graph)和赋权图(Weighted Graph)。前者的边仅仅代表两节点间有关系;而后者则可以表示这个关系的具体的量化值 - 按连通性分类
分为连通图(Connected Graph)和非连通图(Disconnected Graph),前者表示图中任意两点间都存在一条路径;而后者表示至少有两个点间无路径相连。至于更多的细节如强连通、北连通和单向连通等,此处不展开 - 按顶集合大小分类
可分为有限图(Finite Graph)和无限图(Infinite Graph)。前者表示图和边都是有限集合;而后者表示二者皆为无限集合。不过实践中一般研究的都是有限图 - 按顶点和边的特征分类
这种方法可划分为零图(Null Graph)、完全图(Complete Graph)、正则图(Regular Graph)和二分图(Bipartite Graph)等。零图表示只顶点没有边;完全图表示任意两个不同顶点间恰好有一条边;正则图则表示所有顶点度都相等;二分图则表示顶点集合可以划分成两个互不相交的子集,且所有连年两个端点分别属于这两个不同的子集 - 按边的重数和环分类
它可以分为简单图(Simple Graph)、多重图(Multigraph),前者是严格的图定义,不包含环并且顶点间最多只有一条边;多重图则允许两个顶点间存在多条边,但一般不允许有环。另外,如果这个图既有环也有多个边,也可以称为最宽松的图的定义
大家可以对照自己的实际应用中的使用的哪种分类,这样可以更好的理解相关的图的应用。
四、应用场景
图虽然说开发很难遇到,但实际应用场景却非常多。常见的给出以下几类,大家可以自行进行补充:
- 图数据库
- 并行计算中的任务调度
- 地图算法中的最短路径
- AI和大模型底层的图神经网络及基础的近邻图算法等
- 网络路由的链路相关图的算法
- 区块链POW工作量证明中(如以太坊)的DAG算法
其实在实际应用中,图的应用非常多,但大多出现在两个层面,一个是教学层面;另外一个是框架和库的底层算法实现。这也是总说应用非常多,但真正开发遇到的很少的原因。
五、C++例程
在C++中,地图和游戏的查找算法中,算是应用比较多的情况。看一个最短路径的算法:
#include <algorithm>
#include <iostream>
#include <limits>
#include <queue>
#include <stdexcept>
#include <vector>
class DirectedGraph {
public:
struct Edge {
int to;
int weight;
};
explicit DirectedGraph(int vertexCount) : adj(vertexCount) {
if (vertexCount <= 0) {
std::cout<<"vertex count less than zero! "<<std::endl;
}
}
void addEdge(int from, int to, int weight) {
checkVertex(from);
checkVertex(to);
if (weight < 0) {
return;
}
adj[from].push_back({to, weight});
}
int size() const { return static_cast<int>(adj.size()); }
const std::vector<Edge> &neighbors(int vertex) const {
checkVertex(vertex);
return adj[vertex];
}
private:
std::vector<std::vector<Edge>> adj;
void checkVertex(int vertex) const {
if (vertex < 0 || vertex >= static_cast<int>(adj.size())) {
return;
}
}
};
class DijkstraSearch {
public:
static constexpr int INF = std::numeric_limits<int>::max();
struct RetData {
std::vector<int> distance;
std::vector<int> previous;
};
static RetData shortestPath(const DirectedGraph &graph, int source) {
int n = graph.size();
if (source < 0 || source >= n) {
return {std::vector<int>(1, 0), std::vector<int>(1, -1)};
}
std::vector<int> dist(n, INF);
std::vector<int> prev(n, -1);
using Node = std::pair<int, int>;
std::priority_queue<Node, std::vector<Node>, std::greater<Node>> pq;
dist[source] = 0;
pq.push({0, source});
while (!pq.empty()) {
auto [currentDist, u] = pq.top();
pq.pop();
if (currentDist > dist[u]) {
continue;
}
for (const auto &edge : graph.neighbors(u)) {
int v = edge.to;
int weight = edge.weight;
if (dist[u] != INF && dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
prev[v] = u;
pq.push({dist[v], v});
}
}
}
return {dist, prev};
}
static std::vector<int> buildPath(int target, const std::vector<int> &previous) {
std::vector<int> path;
for (int cur = target; cur != -1; cur = previous[cur]) {
path.push_back(cur);
}
std::reverse(path.begin(), path.end());
return path;
}
};
int main() {
DirectedGraph graph(6);
graph.addEdge(0, 1, 9);
graph.addEdge(0, 2, 6);
graph.addEdge(1, 2, 1);
graph.addEdge(1, 3, 2);
graph.addEdge(2, 1, 4);
graph.addEdge(2, 3, 8);
graph.addEdge(2, 4, 2);
graph.addEdge(3, 4, 7);
graph.addEdge(4, 3, 12);
graph.addEdge(4, 5, 5);
int source = 0;
auto ret = DijkstraSearch::shortestPath(graph, source);
for (int i = 0; i < graph.size(); ++i) {
std::cout << "0 -> " << i << " distance: ";
if (ret.distance[i] == DijkstraSearch::INF) {
std::cout << "unreachable" << std::endl;
continue;
}
std::cout << ret.distance[i] << ", path: ";
auto path = DijkstraSearch::buildPath(i, ret.previous);
for (size_t j = 0; j < path.size(); ++j) {
std::cout << path[j];
if (j + 1 < path.size()) {
std::cout << " -> ";
}
}
std::cout << std::endl;
}
return 0;
}
上面的代码是一个简单的迪杰斯特拉的算法C++实现,没有什么难度,上机运行一下就明白了。
六、总结
其实很多框架和库的实现的算法和数据结构中图并不少见,而且非常重要。但真正去做这些的人往往很少。所以开发者不必谈图色变,有什么畏难情绪。在后面如果有机会,再专门对图进行成系列的应用分析和说明。

3586

被折叠的 条评论
为什么被折叠?



