在做图论题的时候,关于如何存图,经常会很纠结。用邻接矩阵,可能会很浪费空间;用邻接表,结构很紧凑,但是链表结构又较难实现。所以,网上有大牛发明了“前向星”,也就是静态链表,即用数组实现邻接表的功能。下面介绍一下我对前向星的一些理解。
注:由于前向星其实和邻接表的原理相同,只不过是用数组实现,因此本文在解释前向星原理的时候经常与邻接表进行比较。希望读者在看本文之前先理解邻接表的原理和实现。
注二:对于树同样可以静态建树,可以参见我的另一篇文章https://www.jianshu.com/p/7e6488e40863
图结构示例如下:
如果使用邻接表存储,结构如下
邻接表.png
可以看出,邻接链表在每个顶点后接一个链表,表示这个顶点的邻接点。那么,如果合理地使用数组存储这个链表,显然更为优越——既能保证不浪费空间,实现起来也比链表容易。
于是,前向星应运而生。前向星和邻接链表不同的一点就是,对于每个顶点,前向星存储的是该顶点的邻接边而非邻接点,也就是说,相比于邻接表在每个顶点后挂上一串该顶点的邻接点,前向星在每个顶点后面“挂”上一串该顶点的邻接边。这里的后一个“挂”是不准确的,我们可以在脑海中可以想象这个结构,但要注意实现“前向星”的数据结构并非如此。
以下是实现前向星的数据结构
//存边的信息的结构体
struct Edge
{
int to;//某个顶点u的邻接点
int next;//顶点u的下一条邻接边的编号
int val;//该邻接边的权值
};
//两个必要的数组
Edge edge[edgenumber];//edge[i]表示编号为i的边
int head[vexnumber];//head[u]表示u的其中一条邻接边的编号
只看上面的数据结构,相信大家还是很疑惑:
为什么Edge结构体不用记录顶点u?
两个数组又有什么用?
不要着急,我下面会仔细分析这些问题。
正如我上面所说,前向星存储的是每个顶点的邻接边。可以在脑海中想象一下,在每个顶点后面连一个链表,不过链表的节点表示的是邻接边而非邻接点。而用数组实现这个链表,就是前向星。
为什么不要存储顶点u?
不管用哪种结构存图,都应该是每读入一条边的信息,就更新一次图结构。读入一条边u-->v,那么我们在存储图的时候,对于顶点u,该边就是u的一条邻接边,我们就把这条边“挂”在u后面。等读入所有的边,建图完成,顶点u后“挂”的边就全是顶点u的邻接边,因此只需要记录这些邻接边的另一个顶点,而不需要存储顶点u。
由于边是按顺序一条一条读入,我们就很自然的想到对边进行编号,然后将这些边存入edge数组。对于Edge结构体中的next,其实就相当于链表中的next指针。不过此处的next是int型变量,存储的是下一条邻接边的编号。
那么head数组是干什么的呢?
这个就相当于邻接链表中的表头数组,head[u]就表示顶点u的某一条邻接边。根据这条边的next,就能找出顶点u所有的邻接边。
如果还是很难想象,没关系,我们可以先来模拟一下。对于上文的示例,作为无向图,假设边权全为1,如果题目给数据,会给出如下数据,由于已经说明是无向图,就不必每条边都正反给出两次了。
5 5 //顶点数和边数
1 2 1
1 4 1
2 3 1
2 5 1
3 5 1
我先给出初始化和建图的代码,然后再做解释。
for(int i=0;i<vexnumber;i++)
{
head[i]=-1;
}
cin>>edgenum;
//由于是无向图,要正向反向都建边
for(int i=0;i<2*edgenum;i+=2)
{
int from,to,v;
cin>>from>>to>>v;
//第一条
edge[i].to=to;
edge[i].val=v;
edge[i].next=head[from];//类似链表头插
head[from]=i;
//第二条
edge[i+1].to=from;
edge[i+1].val=v;
edge[i+1].next=head[to];
head[to]=i+1;
}
以下是模拟建图的过程
需要注意的是,下文edge数组图片中的from->to列并不出现在Edge结构体中,我将其写出来仅仅是为了直观的展示这条边的两个顶点。
初始情况
初始化head数组为-1,类似链表中的NULL。
初始edge数组.png
初始head数组.png
读入第一条边
由于第一条边是 1--2,而且图是无向图,所以要在1--2和2--1建边。更新edge[i].next为head[u],同时更新head数组,使其值为相应的边的编号。这个操作类似链表的头插操作。
edge数组.png
head数组.png
读入第二条边
edge数组.png head数组.png
读入第三条边
edge数组.png head数组.png
读入第四条边
edge数组.png head数组.png
读入第五条边
edge数组.png head数组.png
这样建图就完成了,为了直观,画成邻接表形式,但注意下图并非真正的存储结构。
图的逻辑结构.png
对于图论题,遍历图是经常要做的操作。对于前向星,遍历图的操作依然类似邻接表。以下是遍历图的代码
for(int cur=1;cur<=vexnum;cur++)
{
for(int k=head[cur];k!=-1;k=edge[k].next)
{
//相应操作
//......
//......
}
}
思考一下就能看出,其中cur表示顶点,而k为cur的邻接边在edge数组中的下标。
仅仅只是看了数组中存储的东西,应该还是很难完全理解。因此希望读者自己模拟一遍,加深理解。
以上就是我对前向星的理解。其实前向星采用的就是邻接链表的思想,只不过存储的是顶点的邻接边,并且用数组实现。总而言之,前向星节省空间,便与实现,是存储图的很优秀的数据结构。只要理解其原理,在做赛题的时候省时省力,事半功倍。
网友评论