聊聊"图"数据结构

384 阅读3分钟

在我们日常的编码过程中,遇到稍微复杂一点的算法都会涉及都“树”,“图”这些底层的数据结构。比如我们常常用到的拓扑排序算法,深度优先算法,广度优先算法等等都是基于“图”来设计的。

一、图的定义

图是一种非线性表数据结构。图中的元素我们就叫做顶点,图中的一个顶点可以与任意其他顶点建立连接关系,我们把这种建立的关系叫做

image.png

在我们的微信朋友圈里面,你跟多少朋友有关联关系就可以基于这种方法表示。每个用户有多少个好友,对应到图中,就叫做顶点的,就是跟顶点相连接的边的条数。

如果把上面图的关系引入方向的概念,表示你关注了谁,以及谁关注了你,上面那种无向图就变成了有向图。

image.png

无向图中“度”表示一个顶点有多少条边。在有向图中,我们把度分为入度出度

  • 入度:表示有多少条边指向这个顶点
  • 出度:表示有多少条边以这个顶点为起点指向其他顶点

如果你的社交关系要更复杂一点,你跟每个好友关系的亲密度是不同的,这个时候就要引入“带权图”,通过权重来表示好友间的亲密度。

image.png

二、图的常见存储方式

2.1、图最直观的一种存储方法就是邻接矩阵

邻接矩阵底层依赖一个二维数组。

  • 对于无向图来说:顶点i和顶点j之间有边,则将A[i][j]和A[j][i]标记为1
  • 对于有向图来说:顶点i指向顶点j,则将A[i][j]标记为1
  • 对于带权图,数组中就存储相应的权重

image.png

邻接矩阵存储优点:

  1. 存储方式简单、直接、由于是数组,获取两个顶点之间的关系,就非常高效
  2. 方便计算,可以将图的运算转换成矩阵之间的运算 邻接矩阵存储缺点:
  • 对于顶点很多,边不多时,存储方法浪费空间

2.2、邻接表存储方法

由于邻接矩阵存储比较浪费内存空间,我们可以来看另一种图的存储方法,邻接表。

image.png

时间、空间复杂度一般很难都达到最佳,都是进行互换的一种设计思路。

  • 邻接矩阵存储起来比较浪费空间,但是使用起来比较节省时间。
  • 邻接表存储起来比较节省空间,但是使用起来就比较耗费时间。

三、示例 检测图中的环路

基于拓扑排序判断一个图中是否存在环路的方法。

3.1、无向图判断方法

  1. 求出图中所有顶点的度。
  2. 将所有度<=1的顶点入队。(独立顶点的度为0)
  3. 当队列不空时,弹出队首顶点,把与队首顶点相邻顶点的度减1。如果相邻顶点的度变为1,则将相邻顶点入队列。
  4. 循环结束时判断已经访问的顶点数是否等于全部顶点数,等于则无环,反之则有环。

3.2、有向图判断方法

与无向图相比较,无向图是将所有度<=1的顶点入队;而有向图是将所有入度=0的顶点入队。

题目如下:

image.png

请帮忙检测线路上是否存在环形通路。

image.png

func hasCycle(graph string) bool {
   paths := strings.Split(graph, ",")
   //记录所有顶点
   allV := make(map[string]bool, 0)
   //记录顶点之间的关系
   rel := make(map[string][]string, 0)
   //记录每个顶点的入度
   relInt := make(map[string]int, 0)
   for _, path := range paths {
      ta := strings.Split(path, "->")
      x, y := ta[0], ta[1]
      allV[x], allV[y] = true, true
      if _, ok := rel[x]; !ok {
         rel[x] = make([]string, 0)
      }
      rel[x] = append(rel[x], y)
      relInt[y]++
   }
   //每次遍历入度=0的顶点
   queue := make([]string, 0)
   //记录所有访问过的顶点
   visited := make(map[string]bool, 0)
   for v := range rel {
      if _, ok := relInt[v]; !ok {
         queue = append(queue, v)
         visited[v] = true
      }
   }
   for len(queue) > 0 {
      v := queue[0]
      queue = queue[1:]
      for _, y := range rel[v] {
         relInt[y]--
         if relInt[y] == 0 {
            queue = append(queue, y)
            visited[y] = true
         }
      }
   }
   //访问过的顶点是否等于所有顶点
   return len(visited) != len(allV)
}

四、 总结

业精于勤而荒于喜,不同的数据结构和算法都需要勤加使用才会融汇贯通。人的顿悟是很难速成的,不去经历和磨练,你无法深刻,无法融入自己真正的内心。