在我们日常的编码过程中,遇到稍微复杂一点的算法都会涉及都“树”,“图”这些底层的数据结构。比如我们常常用到的拓扑排序算法,深度优先算法,广度优先算法等等都是基于“图”来设计的。
一、图的定义
图是一种非线性表数据结构。图中的元素我们就叫做顶点,图中的一个顶点可以与任意其他顶点建立连接关系,我们把这种建立的关系叫做边。
在我们的微信朋友圈里面,你跟多少朋友有关联关系就可以基于这种方法表示。每个用户有多少个好友,对应到图中,就叫做顶点的度,就是跟顶点相连接的边的条数。
如果把上面图的关系引入方向的概念,表示你关注了谁,以及谁关注了你,上面那种无向图就变成了有向图。
无向图中“度”表示一个顶点有多少条边。在有向图中,我们把度分为入度和出度。
- 入度:表示有多少条边指向这个顶点
- 出度:表示有多少条边以这个顶点为起点指向其他顶点
如果你的社交关系要更复杂一点,你跟每个好友关系的亲密度是不同的,这个时候就要引入“带权图”,通过权重来表示好友间的亲密度。
二、图的常见存储方式
2.1、图最直观的一种存储方法就是邻接矩阵
邻接矩阵底层依赖一个二维数组。
- 对于无向图来说:顶点i和顶点j之间有边,则将A[i][j]和A[j][i]标记为1
- 对于有向图来说:顶点i指向顶点j,则将A[i][j]标记为1
- 对于带权图,数组中就存储相应的权重
邻接矩阵存储优点:
- 存储方式简单、直接、由于是数组,获取两个顶点之间的关系,就非常高效
- 方便计算,可以将图的运算转换成矩阵之间的运算 邻接矩阵存储缺点:
- 对于顶点很多,边不多时,存储方法浪费空间
2.2、邻接表存储方法
由于邻接矩阵存储比较浪费内存空间,我们可以来看另一种图的存储方法,邻接表。
时间、空间复杂度一般很难都达到最佳,都是进行互换的一种设计思路。
- 邻接矩阵存储起来比较浪费空间,但是使用起来比较节省时间。
- 邻接表存储起来比较节省空间,但是使用起来就比较耗费时间。
三、示例 检测图中的环路
基于拓扑排序判断一个图中是否存在环路的方法。
3.1、无向图判断方法
- 求出图中所有顶点的度。
- 将所有度<=1的顶点入队。(独立顶点的度为0)
- 当队列不空时,弹出队首顶点,把与队首顶点相邻顶点的度减1。如果相邻顶点的度变为1,则将相邻顶点入队列。
- 循环结束时判断已经访问的顶点数是否等于全部顶点数,等于则无环,反之则有环。
3.2、有向图判断方法
与无向图相比较,无向图是将所有度<=1的顶点入队;而有向图是将所有入度=0的顶点入队。
题目如下:
请帮忙检测线路上是否存在环形通路。
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)
}
四、 总结
业精于勤而荒于喜,不同的数据结构和算法都需要勤加使用才会融汇贯通。人的顿悟是很难速成的,不去经历和磨练,你无法深刻,无法融入自己真正的内心。