【数据结构】 对于“前向星”的理解

在做图论题的时候,关于如何存图,经常会很纠结。用邻接矩阵,可能会很浪费空间;用邻接表,结构很紧凑,但是链表结构又较难实现。所以,网上有大牛发明了“前向星”,也就是静态链表,即用数组实现邻接表的功能。下面介绍一下我对前向星的一些理解。
注:由于前向星其实和邻接表的原理相同,只不过是用数组实现,因此本文在解释前向星原理的时候经常与邻接表进行比较。希望读者在看本文之前先理解邻接表的原理和实现。
注二:对于树同样可以静态建树,可以参见我的另一篇文章https://www.jianshu.com/p/7e6488e40863
图结构示例如下:

《【数据结构】 对于“前向星”的理解》 示例.png

如果使用邻接表存储,结构如下

《【数据结构】 对于“前向星”的理解》 邻接表.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数组中的下标。
仅仅只是看了数组中存储的东西,应该还是很难完全理解。因此希望读者自己模拟一遍,加深理解。
以上就是我对前向星的理解。其实前向星采用的就是邻接链表的思想,只不过存储的是顶点的邻接边,并且用数组实现。总而言之,前向星节省空间,便与实现,是存储图的很优秀的数据结构。只要理解其原理,在做赛题的时候省时省力,事半功倍。

    原文作者:tysnd
    原文地址: https://www.jianshu.com/p/60396b6c21c0
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞