如何理解图
图和树比起来是一种更加复杂的非线性表结构。
树中的元素我们称为节点,图中的元素我们叫做顶点。
从图中可以看出来,图中的一个顶点可以与任意其他顶点建立连接关系。我们把这种建立的关系叫做边。
顶点的度:与顶点相连的边的条数。
比如微信,我们可以把每个用户看作一个顶点。如果两个用户之间互加好友,那就在两者之间建立一条边。所以,整个微信的好友关系就可以用一张图来表示。其中,每个用户有多少个好友,对应到图中,就是顶点的度。
边有方向的图叫做有向图,边没有方向的图叫做无向图。
在有向图中我们把度分为入度(In-degree)和出度(Out-degree)。
顶点的入度,表示有多少条边指向这个顶点;顶点的出度,表示有多少条边是以这个顶点为起点指向其他顶点。
对应到微博的例子,入度就表示有多少粉丝,出度就表示关注了多少人。
带权图,在带权图中,每条边都有一个权重,例如QQ中的亲密度这一功能,我们可以通过这个权重来表示QQ好友间的亲密度。
如何在内存中存储图这一数据结构呢?
邻接矩阵存储方法
图最直观的一种存储方法就是,邻接矩阵。
邻接矩阵的底层依赖一个二维数组。
对于无向图来说,如果顶点i与j之间有边,我们就将A[i][j]和A[j][i]标记为1;对于有向图来说,如果顶点i到顶点j之间,有一条箭头从顶点i指向顶点j的边,那我们就将A[i][j]标记为1。同理,如果有一条箭头从顶点j指向顶点i的边,我们就将A[j][i]标记为1。对于带权图,数组中就存储相应的权重。
缺点:
比较浪费存储空间。
比如,对于无向图来说,如果A[i][j]等于1,那么A[j][i]也肯定等于1。实际上,我们只需要存储一个就可以了。
还有,如果我们存储的是稀疏图,即顶点很多,但每个顶点的边并不多,那邻接矩阵的存储方法就更加浪费空间了。
优点:
首先,邻接矩阵的存储方式简单、直接,因为基于数组,所以在获取两个顶点的关系时,就非常高效。
其次,用邻接矩阵存储图,可以将很多图的运算转换成矩阵之间的运算。
邻接表存储方法
乍一看,邻接表挺像散列表的,每个顶点对应一条链表,链表中存储的是与这个顶点相连接的其他顶点。上图中画的是一个有向图的邻接表存储方式,每个顶点对应的链表里面,存储的是指向的顶点。对于无向图来说,每个顶点的链表中存储的是跟这个顶点有边相连的顶点。
邻接矩阵存储起来比较浪费空间,但是使用起来比较节省时间。相反,邻接表存储起来比较节省空间,但是使用起来就比较耗费时间。
邻接表的改进版,即将链表换成更加高效的动态数据结构,比如平衡二叉查找树,跳表,散列表等。
逆邻接表
邻接表中存储用户的关注关系,逆邻接表中存储的是用户的被关注关系。逆邻接表中,每个顶点的链表中存储的是指向这个顶点的顶点。
算法是作用于具体数据结构之上的,深度优先搜索算法和广度优先搜索算法都是基于‘图’这种数据结构的。这是因为,图这种数据结构的表达能力很强,大部分涉及搜索的场景都可以抽象成‘图’。
广度优先搜索(BFS)
直观地讲,它其实就是一种“地毯式”层层推进的搜索策略,即先查找离起始顶点最近的,依次往外搜索。如下图所示:
广度优先搜索的分解图:
最坏情况下,终止顶点t离起始顶点s很远,需要遍历完整个图才能找到。这个时候,每个顶点都要进出一遍队列,每个边也都会被访问一次,所以,广度优先搜索的时间复杂度是O(V+E),其中,V表示顶点的个数,E表示边的个数。
对于一个连通图(一个图中的所有顶点都是连通的)来说,E肯定要大于等于V-1,所以,广度优先搜索的时间复杂度也可以简写为O(E)。
广度优先搜索的空间复杂度是O(V)。
深度优先搜索(DFS)
最直观的例子就是“走迷宫”。假设你站在迷宫的某个岔路口,然后想找到出口。你随意选择一个岔路口来走,走着走着发现走不通的时候,你就回退到上一个岔路口,重新选择一条路继续走,直到最终找到出口。这种走法就是一种深度优先搜索策略。
如何在图中应用深度优先搜索,来找某个顶点到另一个顶点的路径,整个搜索路径如下图所示:
图中实现箭头表示遍历,虚线箭头表示回退。从图中可以看出,深度优先搜索找出来的路径,并不是顶点s到顶点t的最短路径。
实际上,深度优先搜索用的是一种比较著名的算法思想,回溯思想。这种思想解决问题的过程,非常适合用递归来实现。
从上图可以看出,每条边最多会被访问两次,一次是遍历,一次是回退。所以,深度优先搜索算法的时间复杂度是O(E),E表示边的个数。空间复杂度是O(V)。
总结
广度优先搜索需要借助队列来实现,遍历得到的路径就是起始顶点到终止顶点的最短路径。
深度优先搜索是借助栈来实现的。
在执行效率方面,深度优先搜索和广度优先搜索的时间复杂度都是O(E),空间复杂度是O(V)。