一、基本概念
图是一个二元组G=(V,E)。V是非空有穷的顶点集合。E是图G中边的集合。
有向图:图中的每条边都有方向(即带有箭头)。
无向图:图中的每条边都没有方向。
有向边:用尖括号来表示为。a是始点,b是终点。也被称为弧,a是弧尾,b是弧头。
无向边:用圆括号表示为(a,b)。
完全图:任意两个顶点之间都有边的图(有向图或无向图)。
n个顶点的无向完全图有n*(n-1)/2条边;n个顶点的有向完全图有n*(n-1)条边。
顶点的度:与这个顶点邻接的边条数。
对于有向图还有入度和出度的概念。
入度:以此顶点为终点的边的数目。
出度:以此顶点为始点的边的数目。
路径:两顶点之间所存在的通路。
路径的长度:该路径上边的条数。
回路(环):起点和终点相同(重合)的路径。
简单路径:除了顶点和终点可能相同外,其他顶点均不相同的路径。
简单回路:一个回路内,起点和终点相同,但其他的顶点都不相同。也可理解为起点和终点相同的简单路径是简单回路。
简单回路是简单路径的一个子集。
连通:存在从顶点a到b的路径,则说明两顶点是连通的。
连通无向图:无向图中,任意两顶点之间都相互连通。
强连通有向图:有向图中任意两个顶点a、b,从a到b连通,并且从b到a也连通。(因为是有向边,所以从a到b,从b到a的路径都要求存在)
子图:对于图G=(V,E)和G1=(V1,E1),如果V1包含于V,并且E1包含于E,则称G1是G的一个子图。
连通子图:原图可能不是连通图,但其一些子图是连通的,则这些子图称为原图的连通子图。(对于有向图,则称为强连通子图)
极大连通子图(连通分量):G1是原图G的一个连通子图,G1的顶点和边集合都已经不能扩充,是极大的。(如果再增加 顶点,就会不连通,也没有其他的边可加)。如果G本身连通,则只有一个连通分量,就是G本身。
极大强连通子图(强连通分量):这是对于有向图来说的,与无向图中的定义类似。
图4 是强连通有向图,只有一个强连通分量(其自身)。
图2中包含两个强连通分量。顶点a总是与其自身连通,所以其本身就是一个强连通分量。
图3包含3个强连通分量。
图1中任意两个顶点都不是相互连通的,所以有3个强连通分量(3个顶点)。
带权图:图中的每条边都带有一个权值。
网络:带有权值的连通无向图。
二、邻接矩阵及用其实现的图类
邻接矩阵是表示图中顶点间邻接关系的方阵。如果图有n个顶点,那么邻接矩阵就是一个n*n的方阵。
最简单的邻接矩阵是以0/1为元素的方阵。定义如下:
对于带权图,其定义为:
1、以下是有向图的邻接矩阵的一个例子,右图是左图的邻接矩阵。
每行中非零元素的个数对应于顶点i的出度;每列中非零元素的个数对应于顶点i的入度。
2、以下是无向图的邻接矩阵的例子。可以看出,无向图的邻接矩阵是对称矩阵。
邻接矩阵的优点:便于计算出各个顶点的度,以及判断两个顶点间是否有边。
缺点:矩阵中的大部分元素只是为了说明某些边不存在,造成了空间的浪费。
class GraphError(ValueError): pass class Graph: def __init__(self, mat, unconn=0): vnum = len(mat) for x in mat: if len(x) != vnum: # 检查是否为方阵 raise ValueError('Argument for Graph') self.mat = [mat[i][:] for i in range(vnum)] self.unconn = unconn self.vnum = vnum def vertex_num(self): return self.vnum def invalid(self, v): return 0 > v or v >= self.vnum def add_edge(self, vi, vj, val=1): if self.invalid(vi) or self.invalid(vj): raise GraphError(str(vi) + 'or' + str(vj) + 'is not a valid vertex') self.mat[vi][vj] = val def get_edge(self, vi, vj): if self.invalid(vi) or self.invalid(vj): raise GraphError(str(vi) + 'or' + str(vj) + 'is not a valid vertex') return self.mat[vi][vj] # 返回每个顶点的出边的终点位置,和这条出边上的权值 def out_edges(self, vi): if self.invalid(vi): raise GraphError(str(vi) + 'is not a valid vertex') return self._out_edges(self.mat[vi], self.unconn) @staticmethod def _out_edges(row, unconn): edges = [] for i in range(len(row)): if row[i] != unconn: # 当前行中不等于0的位置 edges.append((i, row[i])) return edges
三、用邻接表及其实现的图类
邻接表就是为图中的每个顶点关联一个链表,其中记录这个顶点的所有邻边。
在无向图中,同一条边被邻接表存储两次。在有向图中,同一条边被邻接表存储一次。
在无向图中,顶点的度即为:对应链表中结点的个数。在有向图中,顶点出度即为:对应链表中结点的个数;入度则需要遍历其他顶点的链表。
邻接表的优点:只存储实际的边,节省空间。
缺点:确定顶点的度时,可能需要遍历一个链表。
稠密图一般用邻接矩阵,稀疏图一般用邻接表。
class GraphAL(Graph): def __init__(self, mat1=[], unconn=0): vnum = len(mat1) for x in mat1: if len(x) != vnum: raise ValueError('Argument for Graph') self.mat = [Graph._out_edges(mat1[i], unconn) for i in range(vnum)] self.unconn = unconn self.vnum = vnum def add_vertex(self): self.mat.append([]) self.vnum += 1 return self.vnum - 1 def add_edge(self, vi, vj, val=1): if self.vnum == 0: raise GraphError('can not add edge to empty graph') if self.invalid(vi) or self.invalid(vj): raise GraphError(str(vi) + 'or' + str(vj) + 'is not a valid vertex') row = self.mat[vi] # row是mat中的某一行。例如[(0,4),(2,6)] i = 0 while i < len(row): if row[i][0] == vj: # 如果原来存在vi到vj的边,找出与终点vj相同的终点在第几个元组中 self.mat[vi][i] = (vj, val) return if row[i][0] > vj: # 原来不存在vi到vj的边。因为边表中是按递增的顺序添加的, break # 假设vj=2,但是当前已经遍历到了(3,1),说明没有终点为2的这条边 i += 1 self.mat[vi].insert(i, (vj, val)) def get_edge(self, vi, vj): if self.invalid(vi) or self.invalid(vj): raise GraphError(str(vi) + 'or' + str(vj) + 'is not a valid vertex') for i, val in self.mat[vi]: if i == vj: return val return self.unconn def out_edges(self, vi): if self.invalid(vi): raise GraphError(str(vi) + 'is not a valid vertex') return self.mat[vi] if __name__ == "__main__": mat = [[0, 0, 3], [4, 0, 6], [0, 8, 9]] g = Graph(mat) print(g.out_edges(1)) # [(0, 4), (2, 6)] g1 = GraphAL(mat) g1.add_edge(1, 1, 16) print(g1.mat) # [[(2, 3)], [(0, 4), (1, 16), (2, 6)], [(1, 8), (2, 9)]]