数据结构-图的基本知识和表示
图是一种基本的非线性数据结构,其相关基本概念介绍太多了,这里就不再重复说了。为了让自己能够复习的印象深刻一些,今天专门总结一些图的基本知识。
一、基本的知识点
图包含两个核心要素:顶点和边,一个图可以没有边,但不能没有顶点。
图分可以分为无向图和有向图,有向图就是在这个图中每一条边都有一个方向,表示从一条边出发到另一条边结束。而无向图的边则没有方向。
图的顶点存在“度”这个概念,对于无向图而言,顶点的度即是这个顶点连接的边的条数就是当前顶点的"度";对于有向图而言,顶点的度又分为出度和入度,出度就是从当前顶点出发的边的条数,入度就是以当前顶点为结尾的边的条数。
平行边:两个顶点之间可通过两条或者多条边相连,这些边称之为平行边。
环:一条将顶点连接到它自身的边。
简单图:无环、无平行边。
连通图:图中任意两个顶点之间皆存在路径。
对于一个图G,它的子图的顶点集合是G中顶点的子集,子图的边的集合也是G中边的子集。
对于无向连通图:
回路:始于一个顶点,终于同一个顶点的封闭路径。
无回路的连通图是一棵树。
连通图的生成树:它是图的一个子图,包含了原图的所有顶点,但是包含了保持连通的最少边数。
二、图的表示
表示图就是在程序中存储它的顶点和边,一般使用数组或者线性表存储图的数据结构。
1、表示顶点
顶点可以存储在数组或者线性表中,其中顶点可以是任意类型的对象,也可以是基本数据类型。
2、表示边
边的表示方法有很多,可以是边数组、边对象、邻接矩阵、邻接线性表等。
(1)边数组
就是一个二维数组,第一维的大小是图中边的总数,第二位的大小是2,即边的两个顶点。数组是int型的,存储的边的两个顶点就是顶点数组中对应这两个顶点的索引。
(2)边对象
定义一个边对象,包含两个成员变量u、v,表示边的两个顶点,然后重写equals方法,用于判断两个边对象是否相同。
public class Edge { int u; int v; @Override public boolean equals(Object obj) { return (u == ((Edge)obj).u) && (v == ((Edge)obj).v); } public int getU() { return u; } public void setU(int u) { this.u = u; } public int getV() { return v; } public void setV(int v) { this.v = v; } }
(3)邻接矩阵
邻接矩阵也是一个二维数组,但是不同于边数组的是,它的大小是n*n,n是顶点个数,即如果顶点i到顶点j之间有边,那么array[i][j]=1,否则等于0。对于无向图而言邻接矩阵是对称的,所以为了节省空间,可以采用锯齿矩阵进行存储。对于有向图,因为边是有方向的,所以不存在对称这一概念。
(4)邻接线性表
邻接线性表分为邻接顶点线性表和邻接边线性表。邻接顶点线性表包含了与一个顶点相连的所有顶点。邻接边线性表包含了与一个顶点相连的所有的边。邻接线性表具体的数据结构是,有一个大小为n的线性表数组,这个线性表数组的每一个元素也是一个线性表。
邻接顶点线性表可以定义为:
ArrayList<ArrayList<Integer>> neighbor = new ArrayList<>();
邻接边线性表可以定义为:
ArrayList<ArrayList<Edge>> neighbor = new ArrayList<>();
构建一个邻接边线性表:
neighbor.add(new ArrayList<Edge>()); neighbor.get(0).add(new Edge(0,1)); neighbor.get(0).add(new Edge(0,3)); neighbor.get(0).add(new Edge(0,5)); neighbor.add(new ArrayList<Edge>()); neighbor.get(1).add(new Edge(1,0)); ……
邻接矩阵和邻接线性表都可以表示一个图,但是二者在不同的应用场景下有不同的优点。如果图比较稠密,即含有的边比较多,那么采用邻接矩阵会更好一些,这样的查询效率高,只需常数时间复杂度就可以查询两个点之间是否存在边,并且存储带权重的边也非常方便,但是如果图很稀疏,那么使用邻接矩阵就会很不划算,浪费很多空间,这时使用邻接线性表会更好,能够有效地节省空间。