最近开始学习王争老师的《数据结构与算法之美》,通过总结再加上自己的思考的形式记录这门课程,文章主要作为学习历程的记录。
涉及图的算法有很多,比如图的搜索、最短路径、最小生成树、二分图等。图是一种非线性数据结构。树的元素称为节点,图的元素称为顶点。
图中的顶点可以与任何其他顶点建立连接关系。这种关系称为边。顶点相连接的边的条数称为顶点的度。
实际上,边还有方向的概念。我们把有方向的图称为“有向图”,边没有方向的图叫作“无向图”。下面是一个有向图的表示:
在无向图中有“度”这个概念,表示一个顶点有多少条边。在有向图中,度分为入度和出度。入度指有多少条边指向这个顶点。出度指有多少条边以这个顶点为起点指向其他顶点。
除此之外,还存在一种图——带权图。在带权图中,每个边都有一个权重。如下:
邻接矩阵存储方法
图最直观的一种存储方式即邻接矩阵。
对于无向图,如果顶点i与顶点j之间有边,就将 A [ i ] [ j ] A[i][j] A[i][j]和 A [ j ] [ i ] A[j][i] A[j][i]标记为1。
对于有向图,如果顶点i到顶点j之间,有一条箭头从顶点i指向顶点j的边,就将 A [ i ] [ j ] A[i][j] A[i][j]标记为1。
对于带权图,数组中存储相应的权值。
用邻接矩阵表示图,比较浪费空间。对于无向图,其实只需要利用一半的空间。又比如,如果为稀疏图,即顶点很多,每个顶点的边并不多,就更浪费空间。但邻接矩阵也有优点,它的存储方式简单,直接,基于数组,故在获取两个顶点的关系时,非常高效。其次是方便计算,可将图的运算转换成矩阵之间的运算。
邻接表存储方法
针对上面邻接矩阵比较浪费内存空间的问题,提出了另一种图的存储方法——邻接表。
邻接表的图如下,每个顶点对应着一条链表,链表中存储的是与这个顶点相连接的其他顶点。
每个顶点对应的链表里面存储的是指向顶点。对于无向图,每个顶点的链表中存储的是跟这个顶点有边相连的顶点。
比较邻接矩阵和邻接表,我们可以得出结论:
邻接矩阵存储起来比较浪费,但是使用起来比较节省时间。邻接表存储起来比较节省空间,但是使用起来比较耗时间。
较浪费,但是使用起来比较节省时间。邻接表存储起来比较节省空间,但是使用起来比较耗时间。
什么是搜索算法?
深度优先搜索算法和广度优先搜索算法都是基于“图”这种数据结构。搜索算法即在图中找出从一个顶点出发,到另一个顶点的路径。
首先创建一个无向图:
def createUDG(self,vNum,eNum,v,e): #vNum表示图顶点数,eNum表示图的边数,v表示顶点列表,e表示邻接矩阵
self.vNum = vNum
self.eNum = eNum
self.v = [None]*vNum
for i in range(vNum):
self.v[i] = v[i]
self.e = [[0]*vNum] * vNum
for i in range(eNum):
a,b = e[i]
m,n = self.locateVex(a),self.locateVex(b) #locateVex返回顶点位置
self.e[m][n] = self.e[n][m] = 1
广度优先搜索(BFS)
它是一种“地毯式”层层推进的搜索策略,即先查找离起始顶点最近的,然后是次近的,依次往外搜索。
其编程步骤为:
(1)建立访问标识数组visited[n]并初始化为0,n为图顶点的个数。
(2)将未访问顶点vi入队。
(3)将队首元素vi从队列中取出,依次访问它的未被访问的邻接点vj,vk,…,并将其入队。
(4)重复步骤(3),直至队列为空。
(5)改变i值,0≤i<n,跳出步骤(2)继续进行,直到i=n-1
def BFSTraverse(g):
global visited
visited = [False] * g.getVNum()
for i in range(g.getVNum()):
if not visited[i]:
BFS(g,i)
def BFS(g,i):
q = LinkQueue()
q.offer(i)
while not q.isEmpty():
u = q.poll()
visited[u] = True
print(g.getVex(u),end='')
v = g.firstAdj(u)
while v!=-1:
if not visited[v]:
q.offer(v)
v = g.nextAdj(u,v)
假设图中有V个顶点和E个边。最坏情况下,终止顶点 t 离起始顶点 s 很远,需要遍历完整个图才能找到。这个时候,每个顶点都要进出一遍队列,每个边也都会被访问一次,所以,广度优先搜索的时间复杂度是 O(V+E)。空间复杂度是 O(V)。
深度优先搜索(DFS)
深度优先搜索策略类似于“走迷宫”,随意选择一个岔路口来走,走着走着发现走不通的时候,就退回上一个岔路口,重新选择一条路继续走,直到最终找到出口。以下图为例:
搜索起点为s,终点为t,希望在图中寻找一条以顶点s到顶点t的路径。在图中实线箭头表示遍历,虚线箭头表示回退。深度优先搜索找出来的路径,并不是顶点s到顶点t的最短路径。
实际上,深度优先搜索用的是一种著名的算法思想——回溯思想。这种思想解决问题的过程,非常适合用递归来实现。
其主要步骤:
(1)建立访问标识数组visited[n]并初始化为0,n为图顶点的个数。
(2)以未访问顶点vi为起始点访问其未访问邻接点vj。
(3)从vj出发递归进行步骤(2),直到所有邻接点均被访问。
(4)改变i值,0≤i<n,跳出步骤(2)继续进行,直到i = n-1。
代码实现:
def DFSTraverse(g):
global visited
visited = [False] * g.getVNum()
for i in range(g.getVNum()):
if not visited[i]:
DFS(g,i)
def DFS(g,i):
visited[i] = True
print(g.getVex(i),end = ' ')
v = g.firstAdj(i)
while v!= -1:
if not visited[v]:
DFS(g,v)
v = g.nextAdj(i,v)
假设图有n个顶点和m条边,当图的存储结构是邻接矩阵时需要扫描邻接矩阵的每一个顶点,其时间复杂度为O( n 2 n^2 n2)。当图的存储结构是邻接表时需要扫描每一条单链表,其时间复杂度为O(m)。
总结:广度优先搜索,通俗的理解就是,地毯式层层推进,从起始顶点开始,依次往外遍历。广度优先搜索需要借助队列来实现,遍历得到的路径就是,起始顶点到终止顶点的最短路径。深度优先搜索用的是回溯思想,非常适合用递归实现。换种说法,深度优先搜索是借助栈来实现的。
参考资料:王争《数据结构与算法之美》