说在前面
“链式前向星”,或叫拉链、索引表、静态链表、静态邻接表,等等诸如此类的名称,其实都表示的是利用数组模拟、不采用指针的链表。
链式前向星是建图或者存图的一种的数据结构,在代码编写上由于淘汰了指针更加好写,在执行效率上由于采用了模拟指针的思想效率更高。
在竞赛中面对数据规模较大的题目时采用链式前向星建图有更好的效果,这是由于在数据规模较大如1e5时采用邻接矩阵会爆内存空间而采用邻接链表使用指针极易写错的缘故,而且链式前向星相对于邻接链表可节省更多的空间(指针占用空间较大)。
说说引入
要说链式前向星,先来说“前向星”。前向星是一个特殊的边集数组,使用时先将图中需要存储的边按照起点进行排序,并记录下以某个点为起点的所有边在数组中的起始位置和存储长度,这样就将一张图存储了下来。
定义:head[i]→以i为起点的边集在数组中的起始位置;len[i]→以i为起点的边集在数组中的存储长度。
这样,采用前向星建好图后,只需O(1)的时间就可找到以i为起点的第一条边,而后在O(len[i])的时间内找到以i为起点的所有边。
看起来前向星已经很完美了,而美中不足的是,这个结论建立在排序的基础上。如果要排序,排序这件事本身就需要花费时间代价,而且可能吃力不讨好,需要占用更多的空间。如果不排序,又不能在上述的时间内找到所有的边。
这就需要引入“链式前向星”的概念。
说说实现
“链式前向星”其实就是将链表的思想引入前向星,这样就可以做到不需要排序也能达到同样的效果。
定义:head[i]→以i为起点的最后一条边的编号;e[i].to→以i为起点的边的终点;e[i].next→以i为起点的上一条边的编号。
为方便处理,我们先将head[i]数组初始化为0,并将需要存储的边按1~n进行编号。我们需要做的,就是将每条边的终点存进e[i].to,然后令e[i].next等于head[u](u为当前边的起点,head[u]即表示以u为起点的上一条边的编号),并每次更新head[u]的值(令编号自增即可)。
说说实例
假设当前输入顺序如下(按起点 终点排列):
1 2
2 3
3 4
1 3
4 1
1 5
4 5
按照链式前向星的实现进行操作有:
1 2:e[1].to=2; e[1].next=0; head[1]=1;
2 3:e[2].to=3; e[2].next=0; head[2]=2;
3 4:e[3].to=4; e[3].next=0; head[3]=3;
1 3:e[4].to=3; e[4].next=1; head[1]=4;
4 1:e[5].to=1; e[5].next=0; head[4]=5;
1 5:e[6].to=5; e[6].next=4; head[1]=6;
4 5:e[7].to=5; e[7].next=5; head[4]=7;
这样,我们在遍历时采用双层循环,外层循环对head[i]数组进行遍历,内层循环从j=head[i]到j=e[j].next直到e[j].next=0结束,即可完成对所有以i为起点的边的访问。
在本次实例中找出所有起点为1的边:head[1]=6;
e[6].to=5; e[6].next=4;
e[4].to=3; e[4].next=1;
e[1].to=2; e[1].next=0;遍历结束。
所以起点为1的边有:5,3,2。
说说模板
实现模板:
void init(int u, int v)//u起点,v终点
{
e[k].to = v; //终点,k为编号计数器,初始化为1
e[k].next = head[u];//以u为起点的上一条边的编号
head[u] = k++;//更新以u为起点的最后一条边的编号
}
遍历输出模板:
for (int i = 1; i <= n; i++)//遍历head[i]数组
for (int j = head[i]; j != 0; j = e[j].next)//遍历以i为起点的所有的边
cout << i << " " << e[j].to << endl;
dfs模板:
void dfs(int x)
{
vis[x] = 1; //标记当前节点已被访问,初始化vis[i]数组为0
for(int i = head[x]; i != 0; i = e[i].next)
//对于每个未被访问的相邻节点调用DFS
if(!vis[e[i].to])dfs(e[i].to);
}
bfs模板:
void bfs(int x)
{
queue<int> Q;
for(int i = 1; i <= N; i++)
if(!vis[i])
{
vis[i] = 1;
Q.push(i);
while(!Q.empty())
{
i = Q.front(); Q.pop();//取待扩展的节点,即队头节点
for(int j = head[i]; j != 0; j = e[j].next)//遍历与当前节点相连的节点
if(!vis[e[j].to]){vis[ e[j].to] = 1;Q.push(e[j].to);}
//如果没被访问,入队
}
}
}
说在后面
“链式前向星”在竞赛中是常用的存图方法,可以说掌握它是学习图这种数据结构的开始。由图的链式前向星存储结构,我们还将说到链式前向星在SPFA(求含负权边的单源最短路径的队列优化算法)应用。