概念
图:众所周知图是数据结构中非常重要的一种结构,而且也是比较复杂的。因为在图形结构中,节点间的关系可以是任意的,近几年来图在许多行业迅速发展,所以总的来说掌握图是很重要的。
基本概念
顶点(Vertex) | 图的数据元素 |
弧(Arc) | 图的边(其中在有向图中有弧头(Head)和弧尾(Tail)即一条边的起点和终点) |
无向图 | 图中的边没有方向的指向 |
有向图 | 图中的边有方向 |
完全图 | 有1/2*n(n-1)条边的无向图为完全图 |
有向完全图 | 有n(n-1)条边的有向图为有向完全图 |
稀疏图 | 边数小于nlogn |
稠密图 | 边数大于nlogn |
权 | 图上的边代表的权重 |
邻接点 | 同一条边上的两个点 |
度 | 与图上某个顶点相关联的边的个数(在有向图中还区分入度和出度) |
路径 | 从一个顶点到另一个顶点的一个顶点序列(若首尾顶点相同为回路或环)、顶点不重复出现为简单路径其他同理 |
连通 | 从一个顶点到另一个顶点有路径则连通、图上任意两点都连通则为连通图 |
连通分量 | 无向图中极大连通子图(在加一个点则不为连通图) |
强连通图和强连通分量 | 同理对于有向图而言 |
生成树 | 一个极小连通子图(包含所有顶点且n-1条边) |
图的表示方法
邻接表的核心思想就是针对每个顶点设置一个邻居表。
以上面的图为例,这是一个有向图,分别有顶点a, b, c, d, e, f, g, h共8个顶点。使用邻接表就是针对这8个顶点分别构建邻居表,从而构成一个8个邻居表组成的结构,这个结构就是我们这个图的表示结构或者叫存储结构。
a, b, c, d, e, f, g, h = range(8)
N = [{b, c, d, e, f}, # a 的邻居表
{c, e}, # b 的邻居表
{d}, # c 的邻居表
{e}, # d 的邻居表
{f}, # e 的邻居表
{c, g, h}, # f 的邻居表
{f, h}, # g 的邻居表
{f, g}] # h 的邻居表
这样,N构成了一个邻居节点集。可以通过N对图进行操作了。
# 顶点f的邻居顶点
print(N[f])
# 顶点g是否是a的邻居顶点
print(g in N[a])
# 顶点a的邻居顶点个数
print(len(N[a]))
输出结果:
{2, 6, 7}
False
5
注意:每个顶点的邻居表都是一个集合(set),为什么用set,因为不能重复存储邻居顶点,这是一个非常自然的选择。那么,可不可以用list,当然可以。用字典呢,当然也可以,甚至在表示带权重值的图时,使用字典表示更合理。
N = [{b: 1, c: 2, d: 1, e: 2, f: 3}, # a 的邻居表
{c: 1, e: 2}, # b 的邻居表
{d: 3}, # c 的邻居表
{e: 1}, # d 的邻居表
{f: 2}, # e 的邻居表
{c: 1, g: 1, h: 1}, # f 的邻居表
{f: 1, h: 2}, # g 的邻居表
{f: 1, g: 2}] # h 的邻居表
# 边(a,f)的权重
if f in N[a]:
print(N[a][f])
输出结果:
3
需要注意的是,不管邻居表是用set,list,还是dict,都是邻接表的各种变形,最终使用哪个取决于这个图本身是什么,我们要用这个图干什么。实际应用中我们可以针对图本身特点和我们要解决问题特点针对性的构建图的表示结构。
(2)邻接矩阵
邻接矩阵的核心思想是针对每个顶点设置一个表,这个表包含所有顶点,通过True/False来表示是否是邻居顶点。
还是针对上面的图,分别有顶点a, b, c, d, e, f, g, h共8个顶点。使用邻接矩阵就是针对这8个顶点构建一个8×8的矩阵组成的结构,这个结构就是我们这个图的表示结构或存储结构。
a, b, c, d, e, f, g, h = range(8)
N = [[0, 1, 1, 1, 1, 1, 0, 0], # a的邻接情况
[0, 0, 1, 0, 1, 0, 0, 0], # b 的邻居表
[0, 0, 0, 1, 0, 0, 0, 0], # c 的邻居表
[0, 0, 0, 0, 1, 0, 0, 0], # d 的邻居表
[0, 0, 0, 0, 0, 1, 0, 0], # e 的邻居表
[0, 0, 1, 0, 0, 0, 1, 1], # f 的邻居表
[0, 0, 0, 0, 0, 1, 0, 1], # g 的邻居表
[0, 0, 0, 0, 0, 1, 1, 0]] # h 的邻居表
同样,可以对N进行图操作了,操作方式与邻接表方式有所不同。
# 顶点g是否是a的邻居顶点
print(N[a][g])
# 顶点a的邻居顶点个数
print(sum(N[a]))
# 顶点a的邻居顶点
neighbour = []
for i in range(len(N[f])):
if N[f][i]:
neighbour.append(i)
print(neighbour)
输出结果:
0
5
[2, 6, 7]
在邻接矩阵表示法中,有一些非常实用的特性。
- 首先,可以看出,该矩阵是一个方阵,方阵的维度就是图中顶点的数量,同时还是一个对称矩阵,这样进行处理时非常方便。
- 其次,该矩阵对角线表示的是顶点与顶点自身的关系,一般图不允许出现自关联状态,即自己指向自己的边,那么对角线的元素全部为0;
- 最后,该表示方式可以不用改动即可表示带权值的图,直接将原来存储1的地方修改成相应的权值即可。当然, 0也是权值的一种,而邻接矩阵中0表示不存在这条边。出于实践中的考虑,可以对不存在的边的权值进行修改,将其设置为无穷大或非法的权值,如None,-99999/99999等。
最后总结下,邻接表和邻接矩阵两种表示方法各有特点,具体使用哪个应该针对具体问题具体分析。但事实上,如果不是特别巨大无比的图,用不着费劲思考,用哪种都可以的。
这个实现方式参考:https://blog.csdn.net/saltriver/article/details/54585424
代码实现邻接表:
顶点类
/** * 顶点类 * @project JavaData * @date 2018年4月11日 下午4:31:21 * @author Huaxu-Charles */ public class Vertex { private char label; public Vertex(char label) { this.label = label; } public char getLabel() { return label; } public void setLabel(char label) { this.label = label; } }
/** * 图 * @project JavaData * @date 2018年4月11日 下午4:31:46 * @author Huaxu-Charles */ public class Graph { //顶点数组 private Vertex[] vertexList; //邻接矩阵 private int[][] adjMat; //顶点的最大数目 private int maxSize; //当前顶点 private int nVertex; public Graph() { vertexList = new Vertex[maxSize]; adjMat = new int[maxSize][maxSize]; for(int i = 0; i < maxSize; i++) { for(int j = 0; j < maxSize; j++) { adjMat[i][j] = 0; } } nVertex = 0; maxSize = 100; } /** * 添加顶点 */ public void addVertex(char label) { vertexList[nVertex++] = new Vertex(label); } /** * 添加边 */ public void addEdge(int start,int end) { adjMat[start][end] = 1; adjMat[end][start] = 1; } }
测试:
public class TestGraph { public static void main(String[] args) { Graph g = new Graph(); g.addVertex('A'); g.addVertex('B'); g.addVertex('C'); g.addEdge(0, 1); g.addEdge(0, 2); g.addEdge(1, 2); } }