从零开始的图的存储方法
图论是信息学竞赛中十分重要的一个部分,但许多算法都建立在如何存储图的结构上。
笔者就在这里总结一下图的几种存储方法。
1.邻接矩阵
邻接矩阵是表示图的数据结构中最简单的一种,对于一个有n个点的图,我们需要一个n*n的矩阵,
对于这个图,第i行第j列表示点ai到点aj的距离。
使用邻接矩阵的时候我们需要初始化,map1[i][i]=0,map1[i][j]=∞。每读入一组数据将map1[i][j]赋值为value即可。
对于邻接矩阵,初始化需要O(n^2)的时间,建图需要O(m)的时间,而且空间复杂度是O(n^2)。
虽然邻接矩阵非常简单,但很少有算法选择时间和空间都如此高的邻接矩阵来存储图。
但也有使用邻接矩阵较简单的 如:Floyd
2.前向星
前向星的构造方法很简单,每读入一条边,就将这条边存入数组中,把数组中的边按照起点的顺序排序,就构造完成了。
int head[MAXN];//存储起点为ai的第一条边的位置 struct node { int begin; //起点 int to; //终点 int val; //权值 }; node map1[MAXM];
将所有信息读入后,按到起点为第一顺序,终点为第二顺序,权值为第三顺序排序。
前向星的复杂度与排序的算法有关,一般为O(m*logm)。
因为需要开两个数组,所以空间复杂度为O(n+m)。
3.邻接表
邻接表是图中最常用的存储结构,对于图中的每个顶点ai,把所有邻接于ai的边存入一个单链表,这个单链表就是顶点ai的邻接表。
邻接表实现的方式有很多
如 动态指针建标,vector数组建表,静态建表(与前向星有关)。
这里只介绍后两种。
1.
首先是vector数组建表
数据结构:
struct node { int to; //顶点序号 int val; //权值 }; vector <node> map1[maxn];
遍历
node e; cin>>x>>y>>z; e.to=y; e.val=z; map1[i].push_back(e); //遍历 for(i=1;i<=n;i++) { for(vector <node> ::iterator k=map1[i].begin();k!=map1[i].end();k++) { node t=*k; cout<<i<<" "<<t.to<<" "<<t.val<<endl; } }
使用vector有一点好处,就是空间的申请和释放都不需要自己处理,不容易犯错
2.
静态建标
也有人叫做链式前向星
这种方法是采用用数组模拟链表的策略来建立邻接表
数据结构:
for(i=1;i<=n;i++) { cin>>x>>y>>z; map1[i].to=y; map1[i].val=z; map1[i].next=head[x]; head[x]=i; }
建图与遍历
for(i=1;i<=m;i++) { cin>>x>>y>>z; map1[i].to=j; map1[i].val=z; map1[i].next=head[x]; head[x]=i; } //遍历 for(i=1;i<=n;i++) { for(k=head[i];k!=-1;k=map1[k].next) { cout<<i<<" "<<map1[k].to<<" "<<map1[k].val<<endl; } }
因为我们使用了两个数组,所以空间复杂度是O(n+m),每读入一条边就需要维护数组,所以时间复杂度为O(m)
静态建标的优点在于必要的空间较小,时间复杂度不是很高,代码和原理还有实现都十分明确,而且可以存重边,
除了不能直接用起点和终点确定是否有边以外,其他都是完美的。