数据结构——图

一、介绍

在前面的很多文章中,其实都对图或多或少的进行过分析说明。比如内存的垃圾回收系列、多线程系列以及最新的并行系统中的CUDA和TBB相关的运行图等。
之所以没有认真的分析图相关的数据结构和算法的原因,一个是因为图相对复杂,应用相对专业;另外一个是就是对于大多数的开发者来说,实际遇到图的情况也不多。更多的情况是即使遇到,也大多是简单应用。
本文仍然不会深入展开对图的相关算法和数据结构的详细分析,但会做一个相对整体的总结说明,并对相关的应用进行分析。

二、图

要想谈图,就先得把图的数学定义搞清楚,这样就会有一个严格的限制不会导致表述不清楚引发的各种问题。比如在离散数学中,图的二元组定义为:
一个图𝐺定义为一个有序对G=(V,E),其中:

  • V是一个非空集合,称为顶点集(Vertex Set),其元素称为顶点或节点
  • E 是一个边集(Edge Set),其元素称为边
  • 边与顶点之间存在关联关系:每条边都有它的端点(即连接哪些顶点)。

需要说明的是,这里不是在讨论严格的数学定义,也不对图进行各种方法的展开。即图的三元组定义方式可以参看相关的数学书籍和内容。
从上面的定义可以看出,图其实就是边和顶点的集合。但在图中有几种特殊情况需要注意,一个是环,即在图中形成了一个循环,它包括自身循环和回路;另外一个就是顶点间存在着多个边(多重边)即多重图。
在这里插入图片描述
在这里插入图片描述

其中环开发者还是经常可以遇到的,但多重图就接触的很少了。

三、分类

图在实践中的分类非常灵活,常见的分类方式包括以下几类:

  1. 按边方向分类
    这是最常见的一种情况,即边有方向则为有向图(Directed Graph);无方向则为无向图(Undirected Graph)
  2. 按权重分类
    这种也比较常见,即按边的关系的权重值为划分。可分为无权图(Unweighted Graph)和赋权图(Weighted Graph)。前者的边仅仅代表两节点间有关系;而后者则可以表示这个关系的具体的量化值
  3. 按连通性分类
    分为连通图(Connected Graph)和非连通图(Disconnected Graph),前者表示图中任意两点间都存在一条路径;而后者表示至少有两个点间无路径相连。至于更多的细节如强连通、北连通和单向连通等,此处不展开
  4. 按顶集合大小分类
    可分为有限图(Finite Graph)和无限图(Infinite Graph)。前者表示图和边都是有限集合;而后者表示二者皆为无限集合。不过实践中一般研究的都是有限图
  5. 按顶点和边的特征分类
    这种方法可划分为零图(Null Graph)、完全图(Complete Graph)、正则图(Regular Graph)和二分图(Bipartite Graph)等。零图表示只顶点没有边;完全图表示任意两个不同顶点间恰好有一条边;正则图则表示所有顶点度都相等;二分图则表示顶点集合可以划分成两个互不相交的子集,且所有连年两个端点分别属于这两个不同的子集
  6. 按边的重数和环分类
    它可以分为简单图(Simple Graph)、多重图(Multigraph),前者是严格的图定义,不包含环并且顶点间最多只有一条边;多重图则允许两个顶点间存在多条边,但一般不允许有环。另外,如果这个图既有环也有多个边,也可以称为最宽松的图的定义

大家可以对照自己的实际应用中的使用的哪种分类,这样可以更好的理解相关的图的应用。

四、应用场景

图虽然说开发很难遇到,但实际应用场景却非常多。常见的给出以下几类,大家可以自行进行补充:

  1. 图数据库
  2. 并行计算中的任务调度
  3. 地图算法中的最短路径
  4. AI和大模型底层的图神经网络及基础的近邻图算法等
  5. 网络路由的链路相关图的算法
  6. 区块链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++实现,没有什么难度,上机运行一下就明白了。

六、总结

其实很多框架和库的实现的算法和数据结构中图并不少见,而且非常重要。但真正去做这些的人往往很少。所以开发者不必谈图色变,有什么畏难情绪。在后面如果有机会,再专门对图进行成系列的应用分析和说明。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值