> 本文由 [简悦 SimpRead](http://ksria.com/simpread/) 转码, 原文地址 [www.suibibk.com](https://www.suibibk.com/topic/702275250418614272) > 一、图的存储结构讨论对于线性表来说,是一对一的关系,所以用数组或者链表均可以简单存放。 ### 一、图的存储结构讨论 > 对于线性表来说,是一对一的关系,所以用数组或者链表均可以简单存放。 > 对于树结构是一对多的关系,所以我们要将数组和链表的特性结合在一起才能更好的存放。 > 对于图来说,是多对多的情况,另外图上的任意一个顶点都可以被看做是第一个顶点,任一顶点的邻接点之间也不存在次序关系 **如下图:实际是一个图结构,只不过顶点位置不同。** ![](assets/1587477099286.png) > 由于图的结构复杂,任意两个顶点之间都可能存在联系,因此无法以数据元素在内存中的物理位置来表示元素之间的关系,也就是说,图不可能用简单的顺序存储结构来表示。内存物理位置是线性的,图的元素关系是平面的。 > > 虽然我们可以向树结构中说到的那样使用多重链表,但是我们需要先确定最大的度,然后按照这个度最大的顶点设计结点结构,若是每个顶点的度数相差较大,就会造成大量的存储单元浪费。 ### 二、图的存储结构(1)—- 邻接矩阵 > 考虑到图是由顶点和边(弧)两部分组成的,合在一起是比较困难的,那就很自然的考虑到分为两个结构来分别存储 顶点因为不区分大小,主次,所以用一个一维数组来存储时不错的选择。 > 而边或弧由于是顶点与顶点之间的关系,所以我们最好使用二维数组来存储,图的邻接矩阵存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。 ![](https://www.suibibk.com/fileupload/images/20200421/1587477275442.png) #### 1、无向图 ![](assets/1587477311455-16724777105769-167247771235811.png) ``` 其中1表示两个顶点之间存在关系,0表示无关,不存在顶点间的边。 ``` > 对称矩阵:就是 n 阶矩阵满足 a[i][j]=a[j][i](0<=i,j<=n)。即从矩阵的左上角到右下角的主对角线为轴,右上角的源与左下角相对应的元都是相等的。 根据这个矩阵,我们可以很容易的知道图中的信息。 * 1. 我们要判定容易两顶点是否有边无边就非常容易了。 * 2. 我们要知道某个顶点的度,其实就是这个顶点 vi 在邻接矩阵中第 i 行(或第 i 列)的元素之和。比如上图顶点 v1 的度就是 1+0+1+0=2 * 3. 求顶点 vi 的所有邻接点就是将矩阵第 i 行元素扫描一遍,arc[i][j] 为 1 就是邻接点 #### 2、有向图 对于上面的无向图,二维对称矩阵似乎浪费了一半的空间。若是存放有向图,会更大程度利用起来空间 ![](assets/1587477435893.png) 其中顶点数组是一样的和无向图,弧数组也是一个矩阵,但因为是有向图,所以这个矩阵并不对称:例如 v1->v0 有弧,arc[1][0]=1, 而 v0 到 v1 没有弧,所以 arc[0][1]=0。 另外有向图,要考虑入度和出度,顶点 v1 的入度为 1,正好是第 v1 列的各数之和,顶点 v1 的出度为 2,正好是第 v2 行的各数之和 #### 3、网 每条边上带有权的有向图就叫做网 ![](assets/1587477556167.png) 这里‘∞’表示一个计算机允许的,大于所有边上权值的值 #### 4、Java 实现无向图、有向图、网的邻接矩阵创建 ``` public class G2 { public static void main(String[] args) { //1、无向图 //定点数组vertex String[] v1 = new String[]{"V0","V1","V2","V3"}; //矩阵表示边的关系edge 其中1表示两个顶点之间存在关系,0表示无关,不存在顶点间的边。 int[][] e1 = new int[][] { {0,1,1,1}, {1,0,1,0}, {1,1,0,1}, {1,0,1,0} }; System.out.println("输出无向图:"); for (int i = 0; i < v1.length; i++) { System.out.print(v1[i]+"\t"); } System.out.println(); for (int i = 0; i < v1.length; i++) { for (int j = 0; j < v1.length; j++) { System.out.print(e1[i][j]+"\t"); } System.out.println(); } System.out.println("--------------------"); //2、有向图 String[] v2 =new String[]{"V0","V1","V2","V3"}; int[][] e2 = new int[][] { {0,0,0,1}, {1,0,1,0},//V1的出度为2 {1,1,0,0},//V2的出度为2 {0,0,0,0} //V1的入度为1 }; System.out.println("输出有向图:"); for (int i = 0; i < v2.length; i++) { System.out.print(v2[i]+"\t"); } System.out.println(); for (int i = 0; i < v2.length; i++) { for (int j = 0; j < v2.length; j++) { System.out.print(e2[i][j]+"\t"); } System.out.println(); } System.out.println("--------------------"); //3、网,每条边上带有权的图就叫做网 String[] v3 =new String[]{"V0","V1","V2","V3","V4"}; //m表示不可达 int m=65535; int[][] e3 = new int[][] { {0,m,m,m,6}, {9,0,3,m,m}, {2,m,0,5,m}, {m,m,m,0,1}, {m,m,m,m,0} }; System.out.println("输出网:"); for (int i = 0; i < v3.length; i++) { System.out.print(v3[i]+"\t"); } System.out.println(); for (int i = 0; i < v3.length; i++) { for (int j = 0; j < v3.length; j++) { System.out.print(e3[i][j]+"\t"); } System.out.println(); } } } ``` 代码中的图跟上面说的图一一对应,运行结果如下: ``` 输出无向图: V0 V1 V2 V3 0 1 1 1 1 0 1 0 1 1 0 1 1 0 1 0 -------------------- 输出有向图: V0 V1 V2 V3 0 0 0 1 1 0 1 0 1 1 0 0 0 0 0 0 -------------------- 输出网: V0 V1 V2 V3 V4 0 65535 65535 65535 6 9 0 3 65535 65535 2 65535 0 5 65535 65535 65535 65535 0 1 65535 65535 65535 65535 0 ``` ### 三、图的存储结构(2)—- 邻接表 上面的邻接矩阵是一种不错的图存储结构,便于理解,但是当我们的边数相对于顶点较少的图,这种结构是存在对存储空间的极大的浪费。 ![](assets/1587477763188.png) **我们可以考虑使用链表来动态分配空间,避免使用数组一次分配而造成空间浪费问题。** 同树中,孩子表示法,我们将结点存放入数组,对结点的孩子进行链式存储,不管有多少孩子,都不会存在空间浪费。这种思路也适用于图的存储。我们把这种数组与链表相结合的存储方法称为邻接表。 #### 邻接表处理办法 * 1. 图中顶点用一个一维数组。当然,顶点也可以用单链表来存储,不过数组可以较容易的读取顶点信息,更加方便。另外,对于顶点数组中,每个数据元素还需要存储指向第一个邻接点的指针,以便于查找该顶点的边信息 * 2. 图中每个顶点 vi 的所有邻接点构成一个线性表,由于邻接点的个数不定,所以用单链表存储,无向图称为顶点 vi 的边表,有向图则称为顶点 vi 作为弧尾的出边表 #### 1、无向图 ![](assets/1587478052256.png) 这样的结构,对于我们要获得图的相关信息也是很方便。比如: 我们要获取某个顶点的度,就要去查找这个顶点的边表中结点的个数。 我们要判断 vi 到 vj 是否存在边,只需要测试 vi 的边表链表中是否存在结点 vj 的下标 j 即可。 我们若是要获取顶点的所有邻接点,就是对此顶点的边表进行遍历。 #### 2、有向图 有向图由于有方向,我们是以顶点为弧尾来存储边表的,这样很容易就可以得到每个顶点的出度。但是由于有时也需要确定顶点的入度或以顶点作为弧头的弧,我们可以建立一个有向图的逆邻接表,即对每个顶点 vi 都建立一个链接为 vi 为弧头的表。 ![](assets/1587477964075.png) ![](https://www.suibibk.com/fileupload/images/20200421/1587478082133.png) #### 3、带权值的网 们可以在边表结点定义中再增加一个 weight 数据域,存储权值信息即可。 ![](assets/1587478135713.png) #### 4、Java 实现无向图、有向图、网的邻接链表创建 首先我们看一下《算法导论》中关于图的邻接表的定义:图 G=(V,E) 的邻接表表示有一个包含 |V| 个列表的数组 Adj 所组成,其中每个列表对应于 V 中的一个顶点,对于每一个 u∈V,邻接表 Adj[u] 包含所有满足条件(u,v)∈E 的顶点 v,亦即,Adj[u] 包含图 G 中所有和顶点 u 相邻的顶点。(或者他也可能指向这些顶点的指针),**每个邻接表中的顶点一般以任意的顺序存储。**(注意一下这句话!) 这里的实现方法有多重多样,下面是按我的思路实现的代码示例,一般都会有两个对象,一个是顶点,如下: ``` //定义定点 class Vertex{ String value;//顶点的值 Edge firstEdge;//第一条边 public Vertex(String value, Edge firstEdge) { super(); this.value = value; this.firstEdge = firstEdge; } } ``` 这里我们可已将顶点放在一个数组中,然后每个顶点都从第一条边衍生出去。当然可以按具体需求多加属性,比如顶点的出度也可以当做一个属性什么的,我这里就不加了! 然后定义边的对象: ``` class Edge{ String value;//该边对应的顶点 int weight;//权重,无向图,有向图权重为0 Edge next;//下一个边 /** * 构建一条边 weight未0表示无向图或者有向图 不为0表示网 * @param value * @param weight * @param next */ public Edge(String value, int weight, Edge next) { super(); this.value = value; this.weight = weight; this.next = next; } } ``` 我这里因为要举无向图,有向图,网的例子,所以这个边就直接有权重,但是无向图,有向图的权重为 0。 然后下面是完整的代码,跟上面三个示例图对应: ``` /** * 邻接链表 * @author suibibk.com */ public class Graph { public int num;//顶点个数 //顶点 List vertexs; public Graph(int num) { this.num = num; //初始化图的大小 vertexs = new ArrayList(num); } //插入顶点 public void addVertex(String value) { vertexs.add(new Vertex(value, null)); } //获取顶点 public Vertex getVertex(String value) { for (int i = 0; i < num; i++) { if(vertexs.get(i).value.equals(value)) { return vertexs.get(i); } } return null; } /** * 添加无向图的边 * @param vertex1 第一个顶点 * @param vertex2 第二个顶点 */ public void addUndigraphEdge(String vertex1,String vertex2) { //因为是无向图,所以就直接加入 addEdgeByVertex(vertex1,vertex2,0); addEdgeByVertex(vertex2,vertex1,0); } /** * 添加有向图的边 * @param start 开始节点 * @param end 结束节点 */ public void addDigraphEdge(String start,String end) { //因为是有向图,所以只有一条边 addEdgeByVertex(start,end,0); } /** * 添加网的边 * @param start 开始节点 * @param end 结束节点 * @param weight 权重 */ public void addWebEdge(String start,String end,int weight) { //网就是有向图有权重 addEdgeByVertex(start,end,weight); } /** * 添加一条由start-->end的边 * @param start * @param end * @param weight 权重未0表示无向图或者有向图,部位0表示网 */ private void addEdgeByVertex(String start,String end,int weight) { //1、找到第一个顶点 Vertex v1 = this.getVertex(start); //2、检查这条边是否已经存在,已经存在就直接报错 Edge firstEdge = v1.firstEdge; while(firstEdge!=null) { //获取这个边 String value = firstEdge.value; if(end.equals(value)) { System.out.println("边"+start+"-->"+end+"已经加入,不可以再加入"); return; }else { Edge next = firstEdge.next; firstEdge=next; } } //到这里就可以加入这一条边了 firstEdge = v1.firstEdge; //到这里就可以加入这条边 if(firstEdge==null) { firstEdge = new Edge(end,weight, null); v1.firstEdge=firstEdge; }else { //当前节点变为第一个节点 Edge nowEdge = new Edge(end,weight, null); v1.firstEdge=nowEdge; nowEdge.next=firstEdge; } } /** * 输出图 */ public void print() { for (int i = 0; i "); Edge edge = vertex.firstEdge; while(edge!=null) { System.out.print(edge.value+"["+edge.weight+"]"); Edge next = edge.next; edge=next; if(next!=null) { System.out.print("-->"); }else { System.out.print("-->∧"); } } System.out.println(); } } public static void main(String[] args) { //测试无向图 Graph g = new Graph(4); g.addVertex("V0"); g.addVertex("V1"); g.addVertex("V2"); g.addVertex("V3"); //插入边:无向图 g.addUndigraphEdge("V0", "V1"); g.addUndigraphEdge("V1", "V2"); g.addUndigraphEdge("V2", "V3"); g.addUndigraphEdge("V3", "V0"); g.addUndigraphEdge("V2", "V0"); System.out.println("输出无向图:"); g.print(); System.out.println("------------"); Graph g1 = new Graph(4); g1.addVertex("V0"); g1.addVertex("V1"); g1.addVertex("V2"); g1.addVertex("V3"); //插入边:有向图 g1.addDigraphEdge("V0", "V3"); g1.addDigraphEdge("V1", "V0"); g1.addDigraphEdge("V1", "V2"); g1.addDigraphEdge("V2", "V0"); g1.addDigraphEdge("V2", "V1"); System.out.println("输出有向图:"); g1.print(); System.out.println("----------"); Graph g2 = new Graph(4); g2.addVertex("V0"); g2.addVertex("V1"); g2.addVertex("V2"); g2.addVertex("V3"); //插入边:有向图 g2.addWebEdge("V0", "V4",6); g2.addWebEdge("V1", "V0",9); g2.addWebEdge("V1", "V2",3); g2.addWebEdge("V2", "V0",2); g2.addWebEdge("V2", "V3",5); g2.addWebEdge("V3", "V4",1); System.out.println("输出带权值的网:"); g2.print(); } } //定义定点 class Vertex{ String value;//顶点的值 Edge firstEdge;//第一条边 public Vertex(String value, Edge firstEdge) { super(); this.value = value; this.firstEdge = firstEdge; } } class Edge{ String value;//该边对应的顶点 int weight;//权重,无向图,有向图权重为0 Edge next;//下一个边 /** * 构建一条边 weight未0表示无向图或者有向图 不为0表示网 * @param value * @param weight * @param next */ public Edge(String value, int weight, Edge next) { super(); this.value = value; this.weight = weight; this.next = next; } } ``` Copy 即可运行,结果如下: ``` 输出无向图: V0-->V2[0]-->V3[0]-->V1[0]-->∧ V1-->V2[0]-->V0[0]-->∧ V2-->V0[0]-->V3[0]-->V1[0]-->∧ V3-->V0[0]-->V2[0]-->∧ ------------ 输出有向图: V0-->V3[0]-->∧ V1-->V2[0]-->V0[0]-->∧ V2-->V1[0]-->V0[0]-->∧ V3--> ---------- 输出带权值的网: V0-->V4[6]-->∧ V1-->V2[3]-->V0[9]-->∧ V2-->V3[5]-->V0[2]-->∧ V3-->V4[1]-->∧ ``` 上面把添加变的操作抽取成了公共的方法! 完成!