存图的三种方式
合适的存图方式往往能事半功倍,这里介绍三种方式:邻接矩阵、邻接表、链式前向星。
邻接矩阵
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图论之存图方式