浅谈链式前向星
在介绍链式前向星之前,我们先看一张图:
如果使用邻接表来存储这张图,可能会得到以下的一张表:
顶点 | 第一个相连的点 | 第二个相连的点 | 第三个相连的点 | 第四个相连的点 |
---|---|---|---|---|
1 | 4 | 2 | 5 | \(\tt{NULL}\) |
2 | 3 | \(\tt{NULL}\) | ||
3 | 1 | 5 | \(\tt{NULL}\) | |
4 | \(\tt{NULL}\) | |||
5 | \(\tt{NULL}\) |
链式前向星存储图的方式和邻接表很像,也是以一种链表的形式存储的,但邻接表存储的是点,而链式前向星存储的是边。
顶点 | 第一条相连的边 | 第二条相连的边 | 第三条相连的边 | 第四条相连的边 |
---|---|---|---|---|
1 | 1 | 4 | 5 | \(\tt{NULL}\) |
2 | 2 | \(\tt{NULL}\) | ||
3 | 3 | 6 | \(\tt{NULL}\) | |
4 | \(\tt{NULL}\) | |||
5 | \(\tt{NULL}\) |
链式前向星有一个 \(\texttt{head[]}\)数组,其作用是存储以该节点作为起点的所有边。
具体来讲,就是把从每个节点 \(\tt u\) 出发的边用链表链起来,这样只要记录 \(\texttt{head[u]}\)表示节点 \(\tt u\) 边表的第一条边是谁就可以了。
节点 | 边表中的第一条边 | 边表中的第二条边 | 边表中的第三条边 | 边表中的第四条边 |
---|---|---|---|---|
\(\texttt{u}\) | \(\texttt{head[u]}\) | \(\texttt{next[head[u]]}\) | \(\texttt{next[next[head[u]]]}\) | \(\texttt{next[next[next[head[u]]]}\) |
1 | 1 | 4 | 5 | \(\tt{NULL}\) |
2 | 2 | \(\tt{NULL}\) | ||
3 | 3 | 6 | \(\tt{NULL}\) |
以此类推……
我习惯将值\(\tt{NULL}\)设为0
具体实现
结构体定义
我们可以用结构体来记录每条边的信息。
由于链式前向星记录的是边,所以需要记录这条边指向的点 \(\tt{to}\)、边表中下一条边的编号 \(\tt{next}\)、这条边的权值 \(\tt{w}\)(如果有的话)
struct edge {
int next;
int to;
int w; // 若果有的话
} e[N];
加边函数
int cnt = 0;
void add_edge(int u, int v, int w) {
cnt ++;
e[cnt].to = v;
e[cnt].w = w; // 若果有的话
e[cnt].next = head[u];
head[u] = cnt;
}
其中 \(\tt{cnt}\) 是用来给边编号的,函数里第二行用来记录边的信息,函数里第三行用来在 \(\tt{u}\) 的边表中添加这条边
有了这么一个优秀的加边函数,我们就可以轻松完成一些图的读入和建图操作了。
如要读入一个有 \(\tt{n}\) 个节点,\(\tt{m}\) 条边的有向图:
int n, m;
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i ++) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
add_edge(u, v, w);
}
...
return 0;
}
再如要读入一个有 \(\tt{n}\) 个节点,\(\tt{m}\) 条边的无向图:
int n, m;
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i ++) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
add_edge(u, v, w);
add_edge(v, u, w);
}
...
return 0;
}
其实本质就是创建一个正向和反向两个有向边,合起来就是无向边了。
点邻边的遍历
在图被建立好后,我们只需要读取每个节点 \(\tt{u}\) 的 \(\texttt{head[u]}\),即从节点 \(\tt{u}\) 出发的第一条边,之后循环查找下一条边,直到遇到 \(\tt{NULL}\)(即0)
for (int i = head[u]; i != 0; i = e[i].next) {
...
}
我们将上面的一些代码组合一下,就实现了这个输入点数与边数(其实边数没用),按照点的顺序输出每一个点出发的边的功能。
#include <bits/stdc++.h>
using namespace std;
#define N 10005
struct edge {
int next, to, w;
} e[N];
int head[N];
int cnt = 0;
void add_edge(int u, int v, int w) {
cnt ++;
e[cnt].to = v, e[cnt].w = w;
e[cnt].next = head[u], head[u] = cnt;
}
int n, m;
int u, v, w;
int main() {
memset(head, 0, sizeof(int));
scanf("%d%d", &n, &m)
read(n), read(m);
for (int i = 1; i <= m; i ++) {
scanf("%d%d%d", &u, &v, &w);
add_edge(u, v, w);
}
for (int i = 1; i <= n; i ++)
if (head[i])
for (int j = head[i]; j != 0; j = e[j].next)
printf("%d %d %d\n", i, e[j].to, e[j].c);
else printf("\n");
return 0;
}
dfs 遍历树
我们只需要在遍历点的邻边的基础上加上\(dfs\)即可,函数中 \(\tt{u}\) 为当前节点编号,\(\tt{fa}\) 为父节点编号。无向图记得要需要判断下一个节点是否不为父节点。
void dfs(int u, int fa) {
...
for (int i = head[i]; i != 0; i = e[i].next)
if (e[i].to != fa) // 若为无向图则需加上,防止死循环
dfs(e[i].to, u);
}
dfs 遍历图
遍历图和树的唯一区别就是需要记录每个点在当前递归中是否被走过。
void dfs(int u, int fa) {
...
for (int i = head[i]; i != 0; i = e[i].next)
if (e[i].to != fa && ! vis[e[i].to]){
// e[i].to != fa 一句若为无向图则需加上,防止死循环
vis[e[i].to] = true;
dfs(e[i].to, u);
vis[e[i].to] = false;
}
}
链式前向星与邻接表对比
在了解了链式前向星以后,我们需要了解我们为什么要用它(而不是使用邻接表)。
网上很多资料都有做过比较,比如说这样的:
邻接表是用链表实现的,可以动态的增加边,
而链式前向星是用结构体数组实现的,是静态的,需要一开始知道数据范围,开好数组大小。
相比之下,邻接表灵活,链式前向星好写。
据@LeeCarry大佬原文链接说:\(\texttt{vector}\)邻接表与链式前向星有内存性能上的差异,因为\(\texttt{vector}\)扩充时是默认多申请2倍空间,所以一些特别变态的题目可能会卡内存只能用链式前向星写。他表示最近看\(\texttt{STL}\)源码剖析,\(\texttt{vector}\)每次扩充都要复制一遍元素到新内存块,肯定会慢很多的。
所以\(\texttt{vector}\)邻接表不论是在内存上还是在速度上都是略逊于链式前向星写法的,写法实现上也并没有简洁多少,所以能采用链式前向星的时候(即知道边数时)还是采用链式前向星写法吧。