图的邻接表存储方式:链式前向星

图的邻接表存储方式:链式前向星

注:图片和资料主要参考至这位这位大佬的博客

1. 邻接表

img

图最常见的两种存储方式是邻接表和邻接矩阵。

邻接表存储方式适合存储边稀疏的图,有多少边就使用多少空间,比较节省空间,但是判断两点之间是否有边不方便;【某些时候,若只遍历存在的边,其实复杂度相较于遍历二维矩阵更低】

邻接矩阵适合存储边稠密的,固定的O(N*N)[N为顶点数量]的空间复杂度,若边稀疏很浪费空间,但是判断边和权值都很方便。

邻接表的逻辑结构:

img

主要有两个部分组成:

1)顶点数组;

2)顶点所邻的边形成的链表【所以叫“邻接表” = 邻接点链表 + 节点表】。

乍一看,很像哈希表的数据结构。

存储链表的方式有两种:

1)若不知道边的数量范围,使用node + next指针的经典链表方式;

2)若知道边数量的范围,可以使用数组来代替链表存储。【因为边的数量是有限的,我们只要保证一条边占据数组一个位置,即给边一个唯一的编号,那么边就能用这个唯一的下标来表示一条边】。

数组相较于链表,符合局部性原理,缓存预读的概率更高,往往具有更高的效率。所以优先选择方式2)。

采用数组+数组链表的方式来存储图的邻接表的物理结构叫做链式前向星

好吧,这个名字真够中二的。

2. 链式前向星

先说一下这个存储结构的组成部分:

  1. head数组:记录i节点的编号最大的邻边存储在next中的下标;
  2. next:以数组式链表的形式存储所有边的编号【可能还有其他信息】

这时候,按照next数组的存储单元有两种流派:

1)next存储一个复合的数据结构,假设为Node;

node可以记录i号边指向的顶点,也可以记录权值,记录路径数量等等信息;

2)next单纯存储一个边序号,若需要记录更多的数据,就使用多个数组,保证下标和next同步即可

其中,2)方式书写更简洁,但是不适合存储大量信息的场合,1)方式需要额外定义数据结构,可以按照自己的想法记录数据,适合比较复杂的题目。

这里我们采用方式2)。

1. 初始化

定义三个数组:

EDGE_NUMBER = 5; //边的最大数量,题设一般会给出
NODE_NUMER = 5;//点的最大数量,题设一般会给出    
idx = 0; //记录当前边的自增序号
head = new int[NODE_NUMER];
next = new int[EDGE_NUMBER];
edge = new int[EDGE_NUMBER]; //i号边的入度;
w = new int[EDGE_NUMBER]; //i号边的权值
//同理,若有更多的数据要存储,就定义更多的new int[EDGE_NUMBER]

//更通用的情况下,需要将head初始化为-1,表示这个顶点不存在边
//上图可以不用,只要我们用0表示不存边的顶点即可【若节点从0开始,就只能用-1表示】
//这里我们采用通用的做法
Arrays.fill(head, -1);

由于idx是自增的,因此边的序号不会重复[当然,必须保证每次传入的边不和之前的重复]

注:下图中head标注为了first,因为是直接用的别人的图。

img

假设我们给图一中的图的每条边编号:

1: <1, 4, 9> //分别表示出度from,入度to,权值weight

2: <2, 4, 6>

3: <1, 2, 5>

2. 插入边

链式前向星的核心算法只有两句:

next[++idx] = head[from];
head[from] = idx;

前面说过,head[i]是记录节点i的到目前为止出现的最后一条出度边所在的下标,若这个点没有边,则默认为-1;

那么next[++idx] = head[from]就表示将next数组的范围拓宽一个,并记录下当前插入的这条边的出度的上一条出度边的下标。

同时,from的出度边又新出现了一条,head[from]记录下这最新一条边的地址。

我们也可以记录下额外的信息:

w[idx] = weight;
edge[idx] = to;

idx保持与next中的一致即可。


以上就是添加一条有向边的全部代码了。

我们可以将他封装为一个函数

void add(int from, int to, int weight) {
    next[++idx] = head[from];
	head[from] = idx;
    w[idx] = weight;
	edge[idx] = to;
}

那么记录一条有向边就是一个对称地调用:

add(from, to, weight);
add(to, from, weight);

来看一波插入的图示:

img

插入边<1,4,9>:

  1. 将next的范围扩充一个【下标自增1】,并缓存head[1]之前的数,即-1,表示next[1]存储的是1号顶点的第一条出度边;
  2. 将head[1]置为1,表示1号顶点当前出现的最后一条出度边的信息存储在1号位置【不仅是next,还有w,edge都是如此】

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eklQyP5L-1647671231915)(img/image-20220319140018596.png)]
插入边<2, 4, 6>:

也是类似,此时idx自增到了2;

img

插入边<1, 2, 5>:

  1. 由于1号顶点插入过一次,因此他的first是大于0的,即表示next数组中存储了它的上一条边,我们先使用新拓充的next数组即next[2]去存储上一条边,即next[2] = 1;
  2. 再使用head[1]记录最新一条出度边的下标,即为2;
  3. 在其他数组记录信息。

剩下的也是类似的过程。

3. 遍历

遍历的代码也很固定, 假如要遍历节点i的所有邻边:

for (int j = head[i]; j != -1; j = next[j]) {
    //do sth
}

首先,利用指针j指向i节点的最后一条出度边,由于next[j]记录了倒数第二条边的下标,由此可以得到倒数第二条边…依次类推,最终可以遍历完一个顶点的所有边,直到next[j] == -1为止,表示j已经是i的最后一条出度边了。

全图的遍历,只需要对所有的节点都遍历一次就可以了:

for (int i = 0; i <= NODE_NUMER; i++) {
    for (int j = head[i]; j != -1; j = next[j]) {
    	//do sth
	}
}

遍历的次数就是有向边的数量,即O(EDGE_NUMBER)

3. 使用举例

以leetcode 863为例:
在这里插入图片描述

这题,无论利用树的前序遍历和后序遍历都没法写,因为要综合考虑前面的信息和后面的信息。

我们无法把树当树的题目做,那就转换一种思路,将树看做为图,从target节点开始进行一波搜索,找到距离为k的节点即可;

题设中给出了节点的数量最大值MAX_NODE_NUMBER,且根据二叉树的性质我们可以知道,一个节点最多有两个子节点,由于要考虑前后的关系,我们应该将边看做是无向边,即一个节点最多需要存储四条边,即MAX_NODE_NUMBER * 4就是边的最大数量。

考虑建图: 利用一次前序遍历,将父节点和子节点的连线当做无向边存储进去即可。

考虑解题:如何找到距离为k的边呢?有两种思路:

  1. bfs:利用队列记录第i轮搜索可以触及到所有点,即为距离i的所有顶点;当i==k时,跳出循环,queue中剩余的便是结果集;
  2. dfs:每个位置不断向后搜索,记录没有访问过的邻接点,直到搜索到距离为k,将其加入结果集

bfs和dfs都需要避免将节点重复遍历,所有需要一个额外的flag数组记录访问过的节点。

代码如下:

class Solution {
    /**
         * 存在0节点,那么只能使用-1代替结束了。。。
         */
    private static final int MAX_NODE_NUMBER = 501;

    //点集合,指向当前点的最后一条出现的邻边
    private int[] head = new int[MAX_NODE_NUMBER];

    /**
         * next: j = head[i]指向next中记录的该点的最后一条临边, 则倒数第二条邻边就是next[j]...直到next[?] = 0, 表示不再有邻边
         * edge[j]: 记录边head[j]所对应的邻点
         * idx: 当前next的最大下标
         */
    private int[] next = new int[MAX_NODE_NUMBER * 4];
    private int[] edge = new int[MAX_NODE_NUMBER * 4];
    private int idx = -1;

    /**
         * dfs方式,递归的进行搜索
         * 需要利用一个flag数组记录访问过的点
         */
    private List<Integer> res;
    private boolean[] flag = new boolean[MAX_NODE_NUMBER];
    public List<Integer> distanceK(TreeNode root, TreeNode target, int k) {
        res = new ArrayList<>();
        Arrays.fill(head, -1);
        dfs(root);
        flag[target.val] = true;
        dfsGraph(target.val, k);
        return res;
    }

    private void dfsGraph(int node, int k) {
        if (k == 0) {
            res.add(node);
            return;
        }
        if (head[node] == -1) {
            return;
        }
        for (int i = head[node]; i != -1; i = next[i]) {
            if (flag[edge[i]]) {
                continue;
            }
            flag[edge[i]] = true;
            dfsGraph(edge[i], k - 1);
        }
    }

    /**
         * bfs方式,将邻接点加入queue
         * 需要利用一个flag数组记录访问过的点
         */
    public List<Integer> distanceK1(TreeNode root, TreeNode target, int k) {
        // List<Integer> res = new ArrayList<>();
        Arrays.fill(head, -1);
        dfs(root);
        Queue<Integer> queue = new ArrayDeque<>();
        boolean[] flag = new boolean[MAX_NODE_NUMBER];
        queue.offer(target.val);
        flag[target.val] = true;
        int times = 0;
        while (!queue.isEmpty()) {
            if (times == k) {
                break;
            }
            int n = queue.size();
            for (int i = 0; i < n; i++) {
                int t = queue.poll();
                for (int j = head[t]; j != -1; j = next[j]) {
                    if (!flag[edge[j]]) {
                        flag[edge[j]] = true;
                        queue.offer(edge[j]);
                    }
                }
            }
            times++;
        }
        return new ArrayList<>(queue);
    }

    /**
         * 将二叉树的每个节点与他的两个可能的子节点以`无向边`[2 * 2 = 4]的形式加入链式前向星
         */
    private void dfs(TreeNode root) {
        if (root == null) {
            return;
        }
        if (root.left != null) {
            add(root.val, root.left.val);
            add(root.left.val, root.val);
        }
        if (root.right != null) {
            add(root.val, root.right.val);
            add(root.right.val, root.val);
        }
        dfs(root.left);
        dfs(root.right);
    }

    /**
         * 将边<from, to>加入链式前向星
         * 
         */
    private void add(int from, int to) {
        //next记录上条邻边的地址
        next[++idx] = head[from];
        //head记录当前最后一条临边存储在next中的位置
        head[from] = idx;
        //edge记录最后一条邻边的对点
        edge[idx] = to;
    }
}

ps:可能无法一下子明白邻接点为什么是edge[j], 画图模拟一下插入过程就好理解了。

时间复杂度:O(N)[N为节点个数,插入复杂度为O(N),遍历复杂度为O(N);本身链式前向星的复杂度为O(E),E为边的数量,但是二叉树的边的数量为4N,所以复杂度为O(N)]

空间复杂度:O(N)

4. 总结

  1. 链式前向星是一种完全用数组存储图的邻接表的存储结构,插入、遍历的复杂度都是O(E)[E为边的数量];
  2. 用树的思维无法解决的问题,可以使用图的思维来解决。
  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值