存图的三种方式

合适的存图方式往往能事半功倍,这里介绍三种方式:邻接、邻接表、链式前向星。

邻接矩阵

1)存图思想

用一个矩阵来记录一个图,矩阵第 i 行第 j 列的值就表示顶点 i 到顶点 j 的权值

2  代码实现

 1 #include<stdio.h>
 2 #include<string>
 3 const int V = 1000;  //最大顶点数
 4 int mat[V][V];
 5 
 6 int main()
 7 {
 8     //初始化操作
 9     //假设权值为0表示没有该边
10     memset(mat, 0, sizeof(mat));  
11 
12     //增加边
13     //增加顶点i到顶点j的权值为w的边
14     mat[i][j] = w;
15 
16     //删除边
17     mat[i][j] = 0;
18 
19     //查询边
20     mat[i][j]
21 }

3)优点

a.简单易学

b.对已确定的边进行操作效率高:对已确定的边(两顶点已知),进行增加、删除(或更改权值)、查询,都是O(1)的时间复杂度

4)缺点

a.过高的空间复杂度:这几乎是一个致命的缺点,对于顶点数V,邻接矩阵存图的空间复杂度高达O(V * V),顶点数高于10000就不用考虑了。对于稀疏矩阵,太浪费内存了

邻接表

1)存图思想

对每个顶点,使用不定长的链表来存储从该点出发的所有边的情况,第 i 个链表的第 j 个值就是顶点 i 到顶点 j 的权值。

2)代码实现

 1 #include<stdio.h>
 2 #include<vector>
 3 #include<string>
 4 using namespace std;
 5 
 6 const int V = 100000;
 7 vector<int>e[V];  //定义了V个链表
 8 
 9 int main()
10 {
11     int i,j,k;
12     //邻接链表的初始化
13     //将起点为"i "的链表清空
14     e[i].clear();
15 
16     //增加边
17     //增加顶点"i"到顶点"j"的边
18     e[i].push_back(j);
19 
20     //查询边值
21     //查询以"i"为起点的第"j"条边
22     e[i][j];
23 
24     //寻找权值为k的边
25     for (int j = 0; j < (int)e[i].size(); j++)
26     {
27         if (e[i][j] == k)
28         {
29             //do something
30         }
31     }
32 
33 }

3)优点

a.较为简单易学:数组转链表,定长转不定长

b.内存利用率较高:对于顶点数V、边数E,空间复杂度为O(V + E),能较好的处理稀疏图

c.对不确定的边操作比较方便:比如,要遍历从某点出发的某条边,不会像邻接矩阵一样可能遍历到不存在的边

4)缺点

a.重边不好处理:判重比较麻烦,还要遍历已有的边,不能直接判断;一般情况下使用邻接表存图是会存储重边的,不会做重边的判断

b.对确定边效率不高:比如对于给定i->j的边要进行查询或修改等操作只有通过遍历这种方式找到

链式前向星

1)存图思想

主要的数据结构是边数组(存储边的数组),当然想要完美表示图,光有一个边集数组还不够,还要有一个数组每一个点的第一条边。同时,每一条边都需要存储接下来一条边的“指针”

2)代码实现

 1 #include<stdio.h>
 2 #include<string>
 3 const int V = 100000;
 4 const int E = 100000;
 5 
 6 //边结构体的定义
 7 struct Edge
 8 {
 9     int to;        //该边的另一个顶点
10     int w;        //该边的权值
11     int next;  //下一条边的数组下标
12 };
13 
14 //head[i]表示顶点i的第一条边的数组下标,"-1"表示顶点i没有边
15 int head[V];
16 Edge edge[E];
17 
18 
19 //增加边
20 //新增加边"a -> b",该边的权值为w,数组下标为"id"
21 inline void AddEdge(int a, int b, int w, int id)
22 {
23     edge[id].to = b;
24     edge[id].w = w;
25     edge[id].next = head[a];  // 新增的边要成为顶点`a`的第一条边,而不是最后一条边
26     head[a] = id;              //类似于链表的头插法,最后一条边指向"-1"
27     return;
28 }
29 int main()
30 {
31     int a,b,w;
32     //链式前向星的初始化
33     //只需要初始化顶点数组就可以了
34     memset(head, -1, sizeof(head));
35 
36     //遍历从a出发的所有边
37     for (int i = head[a]; i != -1; i = edge[i].next)
38     {
39         //do something
40         if(edge[i].to == b)  //找到边a -> b
41         if(edge[i].w == w)   //找到权值为w的边
42     }
43 }

3)优点

a.内存利用率高:相比vector实现的邻接表而言,可以准确开辟最多边数的内存,不像vector实现的邻接表有爆内存的风险

b..对不确定的边操作比较方便:和邻接表一样不会遍历到不存在的边

4)缺点

a.操作复杂一点:要求比较熟练,不熟练写起来比较慢

b.重边不好处理:这点与邻接表一样,只有通过遍历判重

c.对确定边操作效率不高:也与邻接表一样,不能通过两点马上确定边,只能遍历查找

总结

对于邻接矩阵,由于内存消耗局限性的,几乎只能用于简单的图论题

邻接表存图是最为常见的一种方法,绝大部分采用C++STL中的vector实现,一般情况下大部分图论题目都能使用该存图方式。

而链式前向星是一种较好的用来代替邻接表的数据结构,在邻接表存图不能使用时可以使用,几乎可以用于全部图论题目。如果熟练掌握,可以作为主要的存图方法。

参考链接:izqt.github.io/2015/07/21/ACM图论之存图方式

 

posted @ 2018-07-16 10:49  Rogn  阅读(3146)  评论(1编辑  收藏  举报