- 顶点(vertex):图中的元素
- 边(edge):一个顶点与任意其他顶点建立连接关系
- 度(degree):跟顶点相连接的边的条数
- 入度(In-degree)
- 出度(Out-degree)
- 有向图:边有方向的图
- 无向图:边没有方向的图
邻接矩阵存储方法
图最直观的一种存储方法就是,邻接矩阵(Adjacency Matrix)。
邻接矩阵的底层依赖一个二维数组。对于无向图来说,如果顶点i与顶点j之间有边,我们就将 A[i][j]
和 A[j][i]
标记为 1
;对于有向图来说,如果顶点i到顶点j之间,有一条箭头从顶点i指向顶点j的边,那我们就将 A[i][j]
标记为 1
。同理,如果有一条箭头从顶点 j
指向顶点i的边,我们就将 A[j][i]
标记为 1
。对于带权图,数组中就存储相应的权重。
稀疏图(Sparse Matrix)浪费存储空间。
邻接表存储方法
邻接表(Adjacency List)
逆邻接表
哈希算法等数据分片方
数据库存储
无向图代码
1 | public class Graph { // 无向图 |
广度优先搜索(BFS)
广度优先搜索(Breadth-First-Search),它其实就是一种“地毯式”层层推进的搜索策略,即先查找离起始顶点最近的,然后是次近的,依次往外搜索。
1 | public void bfs(int s, int t) { |
visited 是用来记录已经被访问的顶点,用来避免顶点被重复访问。如果顶点q被访问,那相应的 visited[q]
会被设置为 true
。
queue 是一个队列,用来存储已经被访问、但相连的顶点还没有被访问的顶点。因为广度优先搜索是逐层访问的,也就是说,我们只有把第k层的顶点都访问完成之后,才能访问第 k+1
层的顶点。当我们访问到第k层的顶点的时候,我们需要把第k层的顶点记录下来,稍后才能通过第k层的顶点来找第 k+1
层的顶点。所以,我们用这个队列来实现记录的功能。
prev 用来记录搜索路径。当我们从顶点s开始,广度优先搜索到顶点t后,prev数组中存储的就是搜索的路径。不过,这个路径是反向存储的。prev[w]
存储的是,顶点 w
是从哪个前驱顶点遍历过来的。比如,我们通过顶点2的邻接表访问到顶点3,那 prev[3]
就等于 2
。为了正向打印出路径,我们需要递归地来打印,你可以看下 print()
函数的实现方式。
最坏情况下,终止顶点t离起始顶点s很远,需要遍历完整个图才能找到。这个时候,每个顶点都要进出一遍队列,每个边也都会被访问一次,所以,广度优先搜索的时间复杂度是O(V+E),其中,V表示顶点的个数,E表示边的个数。当然,对于一个连通图来说,也就是说一个图中的所有顶点都是连通的,E肯定要大于等于V-1,所以,广度优先搜索的时间复杂度也可以简写为O(E)。
广度优先搜索的空间消耗主要在几个辅助变量visited数组、queue队列、prev数组上。这三个存储空间的大小都不会超过顶点的个数,所以空间复杂度是O(V)。
深度优先搜索(DFS)
深度优先搜索(Depth-First-Search),简称DFS。最直观的例子就是“走迷宫”。
1 | boolean found = false; // 全局变量或者类成员变量 |
每条边最多会被访问两次,一次是遍历,一次是回退。所以,图上的深度优先搜索算法的时间复杂度是 O(E)
,E表示边的个数。
深度优先搜索算法的消耗内存主要是 visited
、prev
数组和递归调用栈。
visited
、prev
数组的大小跟顶点的个数V成正比,递归调用栈的最大深度不会超过顶点的个数,所以总的空间复杂度就是 O(V)
。
小结
无向图、有向图、带权图、顶点、边、度、入度、出度。除此之外,我们还学习了图的两个主要的存储方式:邻接矩阵和邻接表。
邻接矩阵存储方法的缺点是比较浪费空间,但是优点是查询效率高,而且方便矩阵运算。邻接表存储方法中每个顶点都对应一个链表,存储与其相连接的其他顶点。尽管邻接表的存储方式比较节省存储空间,但链表不方便查找,所以查询效率没有邻接矩阵存储方式高。针对这个问题,邻接表还有改进升级版,即将链表换成更加高效的动态数据结构,比如平衡二叉查找树、跳表、散列表等。
- 深度:栈
- 广度:队列
广度优先搜索和深度优先搜索是图上的两种最常用、最基本的搜索算法,比起其他高级的搜索算法,比如A、IDA等,要简单粗暴,没有什么优化,所以,也被叫作暴力搜索算法。所以,这两种搜索算法仅适用于状态空间不大,也就是说图不大的搜索。
广度优先搜索,通俗的理解就是,地毯式层层推进,从起始顶点开始,依次往外遍历。广度优先搜索需要借助队列来实现,遍历得到的路径就是,起始顶点到终止顶点的最短路径。深度优先搜索用的是回溯思想,非常适合用递归实现。换种说法,深度优先搜索是借助栈来实现的。在执行效率方面,深度优先和广度优先搜索的时间复杂度都是O(E),空间复杂度是O(V)。