1 图(graph)的定义
2 图的表示
2.1 邻接矩阵
2.1.1 邻接矩阵的优点
在O(1)的时间复杂度下,就可以判断一条边(u,v)是否存在(直接看第u行第v列那个元素即可)
可以做关于矩阵的代数运算
2.1.2 邻接矩阵的缺点
需要Ω(n^2)的空间
不能很高效地看一个点所有的邻边(需要找到这个点所在的一行/一列,然后遍历这一行/这一列),差不多需要O(n)的时间复杂度
2.2 邻接列表
邻接列表的每一个条目里是所有和点v相连的点 组成的列表
2.2.1 邻接列表的优点
弥补了邻接矩阵所有的缺点:
可以很方便地查看一个点所有的邻边(O(1)时间复杂度)
空间复杂度O(n+m),m是图G的边数。
对每一条边(a,b),我会在两个地方存储他的端点。adj[a]=b,adj[b]=a
也就是说,如果我们有m条边,那我们需要O(m)的空间复杂度
再加上adj的n个条目
所以空间复杂度为O(m+n)
2.2.2 邻接列表的缺点
基本上邻接矩阵的优点就是它的缺点
判断两个点之间(eg,v&u)有没有边,需要遍历整个adj[v]中有没有u,这个需要O(n)的时间复杂度
2.3 邻接矩阵和邻接列表的权衡
两种各有利弊,好处坏处互补。我们需要根据实际应用的需求来决定使用那种图的表示方法(eg,图是否稀疏?是否需要频繁找边?)
在现实世界中,由于graph偏稀疏(sparse),同时 不一定有足够的空间来保存邻接矩阵。因此,邻接列表的使用更多一点。
3 连接性
3.1 概念
3.1.1 path
3.1.2 circle
3.1.3 连通图
3.1.4 连通部分
3.1.5 强连通性
3.2 树
3.2.1 树的性质
- 一个有n个节点的树有n-1条边
- 两个点之间的路径是唯一的
比如在一棵树上,我们有不同的两个路径 A-B-C和A-D-C,那么我们就会成环A-B-C-D-A,与无环冲突
4 图的遍历
找出所有和u有路径的点
4.1 最naive的方法
一开始只有一个节点(初始节点)被标记了(visited)
每次遍历所有的边,看是不是一个顶点是visited另一个节点是unvisited,如果是的话,将不是unvisited的节点标记为visited
重复上一步,直到找不到这样的边为止
那么结束循环时候的节点就是所有u可以到达的节点
4.1.1 时间复杂度
我们一步一步看
第一步:——时间复杂度 O(n)
第二步: ——时间复杂度O(1)
第三步:
每一次循环,我们至少可以把一个点标记成visited,所以一共有O(n)次循环(n是图中点的数量)【可能比O(n)少,因为可能一次标记多个点,或者没有等所有的点标记完,就已经结束循环了】
每一次循环内部,我们需要遍历所有的边,看看是不是这条边一个节点是visited另一个节点是unvisited,那么每一次循环,需要O(m)的时间复杂度(m是图中边的数量)
——所以第三步的时间复杂度是O(nm)
因为O(n)肯定比O(nm)小,所以最终总的算法的时间复杂度是O(nm)
4.2 改进方法
4.1的naive方法中,我们有很多次重复地讨论了边的情况,比如如果一条边的两个节点都是visited或者都是unvisited,那么在这一轮循环中,我们理论上是可以不用讨论的,但是4.1的方法中也讨论了。4.2就尝试了去解决边遍历时候的冗余
我们设置了一个集合u,一开始只有初始节点u。
每次从集合中取出一个点,找它的邻边中另外一个节点是unvisted的点。然后将那些点设置为visited,同时将那些点放入这个集合中。
这样循环,直到集合空为止(可能是所有的点都遍历了,可能是剩余的点都不和标记为visited的点有邻边了)
4.2.1 时间复杂度
第一步:——时间复杂度O(1)
第二步:——时间复杂度O(n)
第三步:——时间复杂度O(1)
第四步:‘
这个循环怎么看呢?我们这样想:
每一条图中的边最多被考虑两次。(如果一个节点是u的话,只考虑一次)
一次是边的两个点都是unvisited的时候,如果本轮从R中取出来的点正好和这两个unvisited的点中的一个有连接。那么这个unvisited的点会被设置为visited,并放入R中,下次这个点被拿出来的时候,这条边会被遍历第一次。
第二次就是上一步设置为visited的点,从R中拿出来,并遍历它的邻边的时候,会把这条边的第二个点设置为visited,并放入集合中。那么下次这第二个点被从R中拿出来的时候,这条边又会被遍历一次。(不过由于那个时候这条边的两个点都是visited了,所以不会有任何操作)
所以第四步的时间复杂度是O(m)(m是图的边数量)
所以总的时间复杂度是O(n+m)