学习:数据结构——邻接表

在做图论的时候,为了储存图,我们会使用邻接矩阵

就比如上面这个图,用邻接矩阵储存就是这样:

0 1 2 3 4 5
0 0 0 1 0 1 1
1 0 0 0 0 1 1
2 1 0 0 1 1 0
3 0 0 1 0 0 0
4 1 1 1 0 0 0
5 1 1 0 0 0 0

但是这样储存明显有缺点:

  1. 空间复杂度太大 O(n2)O(n^2)
  2. 时间复杂度太大 O(n2)O(n^2)

于是乎,邻接表值得拥有
谔谔,查遍oi wiki就找到了两行解释,然后百度百科太权威看不懂
好吧,看看广大人民怎么说:

在网上翻半天翻到的

首先我想说邻接表真是个巧妙的数据结构呵


邻接表

首先,邻接表跟链表有几分相像,这样就好理解邻接表了

不过我们不用链表,我们只是用数组来模拟邻接表的普通OIer

那么就有以下数组要用:

  1. u[i]u[i] 存第ii条线的起点
  2. v[i]v[i] 存第ii条线的终点
  3. w[i]w[i] 存第ii条线的权值
  4. first[i]first[i]我其实说不清楚
  5. next[i]next[i]其实我还是说不清楚

还有,设nn为边数,mm为点数

关于uu,vv,ww数组

读入的时候,就把对应的起点、终点、权值存进uu vv ww

关于firstfirst,nextnext数组

最重要的地方来了,就是firstfirstnextnext到底是干啥的呢?

关于firstfirst

firstfirst数组其实是存起点为第ii个点的边的编号

拗口?(反正我这么觉得)

实际操作起来就是这样的:

初始化将firstfirst全部初始化为1-1

每输入一条边(假设输入的边的编号、起点、终点、权值分别为ii,uiu_i,viv_i,wiw_i),first[ui]first[u_i]若等于1-1,就将first[ui]first[u_i]赋为ii

现在根据这个图(权值就看做1)

uu vv ww可以是这样的(反正什么顺序都一样,就字典序吧):

ii 1 2 3 4 5 6 7 8
uu 0 0 0 1 1 2 2 4
vv 2 4 5 4 5 3 4 5
ww 1 1 1 1 1 1 1 1

然后,拿出firstfirst数组,全部初始化为1-1

jj 0 1 2 3 4 5
firstfirst -1 -1 -1 -1 -1 -1

按照步骤,读到i=1i=1的时候firstfirst数组就是这样:

jj 0 1 2 3 4 5
firstfirst 1 -1 -1 -1 -1 -1

可以说,邻接表的操作方式真的很迷惑

但接下来就是更迷惑的nextnext数组了

关于nextnext

当有两条边的起点一样时(设第一个边编号为ii,第二个边编号为jj,公共的起点为xx,就是说 ui=uj=xu_i=u_j=x ),nextnext就发挥了作用

如果我们在这种情况下仍然用上面firstfirst的操作的话,以前的first[x]first[x]自然会被覆盖掉

所以我们把first[x]first[x]存到next[j]next[j]去,再把first[x]first[x]赋为jj

拗口?(反正我还是这么觉得)

实际上就是这样:

初始化时nextnext数组全部初始化为1-1

每次在输入一条边后,如果first[x]first[x]不等于1-1,那就将next[j]next[j]赋为first[x]first[x],再将first[x]first[x]赋为jj

还是接着之前的操作:

jj 0 1 2 3 4 5
firstfirst 1 -1 -1 -1 -1 -1
kk 1 2 3 4 5 6 7 8
nextnext -1 -1 -1 -1 -1 -1 -1 -1

按步骤操作到i=2i=2

jj 0 1 2 3 4 5
firstfirst 2 -1 -1 -1 -1 -1
kk 1 2 3 4 5 6 7 8
nextnext -1 1 -1 -1 -1 -1 -1 -1

真的非常迷惑

但还没完

我们现在把整个弄完:

手动模拟邻接表

i=3i=3
jj 0 1 2 3 4 5
firstfirst 3 -1 -1 -1 -1 -1
kk 1 2 3 4 5 6 7 8
nextnext -1 1 2 -1 -1 -1 -1 -1

i=4i=4
jj 0 1 2 3 4 5
firstfirst 3 4 -1 -1 -1 -1
kk 1 2 3 4 5 6 7 8
nextnext -1 1 2 -1 -1 -1 -1 -1

i=5i=5
jj 0 1 2 3 4 5
firstfirst 3 5 -1 -1 -1 -1
kk 1 2 3 4 5 6 7 8
nextnext -1 1 2 -1 4 -1 -1 -1

i=6i=6
jj 0 1 2 3 4 5
firstfirst 3 5 6 -1 -1 -1
kk 1 2 3 4 5 6 7 8
nextnext -1 1 2 -1 4 -1 -1 -1

i=7i=7
jj 0 1 2 3 4 5
firstfirst 3 5 7 -1 -1 -1
kk 1 2 3 4 5 6 7 8
nextnext -1 1 2 -1 4 -1 6 -1

i=8i=8
jj 0 1 2 3 4 5
firstfirst 3 5 7 -1 8 -1
kk 1 2 3 4 5 6 7 8
nextnext -1 1 2 -1 4 -1 6 -1

上面有一个细节,就是0j<m,1kn0 \le j \lt m, 1 \le k \le n

好了,我们现在存好数组了,那么如何读取呢?

这个费尽心思搞出的储存方式,一定有很好的性能(简洁的算法往往带来低效,复杂的算法往往带来高效)

读取

我们来看,当我们需要遍历以一个点为起点的所有边,我们怎样来读取信息

00为起点为样例:(为了演示就只能用图片了)

黄的表示起点,蓝的表示边的编号,红的表示下一条边的储存位置,黑的表示读取终止

其中我们从firstfirst数组的第00项读起

下一个边的位置便是当前读到的边的编号

直到读到1-1为止

你会发现,读取的效率非常高,因为读取都是一连串读下来,次次读的都是数据

而且空间占用很小

对比一下邻接矩阵,邻接矩阵无地自容