树与图的存储

树与图的存储

我们在算法比赛中都是以数组来模拟 树 与 图。

我们常常使用 链表邻接表来存储树与图。常常看到有人使用结构体来实现链表和邻接表,但是这只适用于面试,不适用与笔试。这是因为每次创建一个新的节点都需要调用 new 函数,然而 new 函数效率是非常低的。低到什么程度?一般题目节点有 10 万 到 100 万,然而你仅仅是 new 10万个节点就有可能超时了……当然,你一开始初始化 10 万个节点,这个倒是可以,但是这和数组就区别不大了。

数组模拟链表(又称为 静态链表)最主要的好处就是快。

单链表

使用背景

**邻接表最常用的用途就是存储 。**因此,单链表在算法题中最常用的就是存储图和树。

单链表主要用来实现邻接表,邻接表主要用来存储 图 和 树。

实现思路

一开始,一个头指针 head。头指针 head 指向空元素。

image-20201019204127989

然后,向链表插入节点,head 依旧指向头元素,链表的尾节点指向空节点。image-20201019204325031

每一个节点上存一个 value 和 next 指针。


使用到的数组

根据上述思路,我们用数组进行模拟。首先,每个节点有有 value 和 next 指针。用数组 e[N] 存储 value,ne[N] 来存储 next 指针。而数组 e 与 数组 ne 使用 相同的下标 进行关联就可以表示同一个节点的不同属性了。空节点可以用 -1 来表示,即链表的尾节点的 ne 元素值为 -1。(为什么要存在尾结点还赋值为 -1?这是为了后期顺序遍历数组时,让遍历的指针知道什么时候应当停止遍历)

image-20201019205013538

const int N = 100010;

int head = -1; // head指针,指向链表头节点:head == 头节点的下标
int e[N]; // 存放每个节点存储的值
int ne[N]; // 存放每个节点 下一个节点的实际下标
int idx = 0; // 存储我们当前已经用到了那个地址(实际下标),也相当于一个指针。
单链表的操作
  1. 单链表初始化操作。单链表一定要记得初始化啊

    //* 链表初始化
    void init() {
        head = -1; // head指针一开始指向空节点
        idx = 0; // 表示链表从数组的 0 号点开始分配
    }
    
  2. 头插法 。所谓头插法即把新的节点插入在head指针指向的头节点之前,所以这个新节点就变成了新的头节点了。

//* 头插法
void add_to_head(int x) {
    e[idx] = x;
    ne[idx] = head;
    head = idx;
    ++idx; // 当前实际位置已经存储了新的节点,idx下标移到下一个位置
}

image-20201019210245768

  1. 插入操作:将 x 这个值插入到 下标 为 k 的结点的后面。

    //* 常规插入操作:在第 k 个节点之后插入 x
    void add(int k, int x){
        e[idx] = x;
        ne[idx] = ne[k];
        ne[k] = idx;
        ++idx;
    }
    

    image-20201019211038800

  2. 删除操作:单链表可以在 O ( 1 ) O(1) O(1) 的时间找到下一个点的位置,但是缺不能在 O ( 1 ) O(1) O(1) 的时间找到上一个点的位置。**单链表只能向后看,不能向前看。**所以我们删除下一个节点。算法题不用担心内存泄漏的问题,没必要。

    //* 单链表的删除操作:将下标是k的点 的后面的节点删除
    void remove(int k) {
        ne[k] = ne[ne[k]];
    }
    

    image-20201019211555653

双链表

背景

双链表主要是用来优化某些题的。

双链表就是每个节点有两个指针,一个指向前,一个指向后,即用 l[N] 存每个节点左边的点是谁,用 r[N] 存每个节点右边的节点是谁。

双链表依旧使用多个 int 数组来实现,而不使用 结构体数组。这是因为使用结构体数组后,一行代码会变得非常的长,没有 int 数组简洁。

使用到的数组

const int N = 100010;
int e[N]; // 存每个节点的值
int l[N]; // 存每个节点前一个节点的指针
int r[N]; // 存每个节点后一个节点的指针
int idx = 2; // 实际存储的数组序号

双链表的操作

初始化

我们用 idx == 0 的数组表示头指针,idx == 1 的数组表示尾部指针。不再像单链表的实现一样使用 head 指针。

image-20201021171432810

void init() {
    r[0] = 1;
    l[1] = 0;
    idx = 2; // 因为 0 、1 序号的数组已经表示头指针和尾部指针了
}
插入点

image-20201021172359151

// 在下标是 k 的点的右边插入 x 
void add(int k, int x) {
    e[idx] = x;
    r[idx] = r[k];
    l[idx] = k;
    l[r[k]] = idx; // 不能与下面一行顺序写反
    r[k] = idx;
    ++idx;
}

//* 在下标是 k 的点的左边插入 x:add(l[k], x);
删除点

让第 k 个点的左端点 的右指针指向 第 k 个点的右端点。让第 k 个点的右端点的左指针指向第 k 个点的左端点。

image-20201021172441314

// 删除第 k 个节点
void remove(int k) {
    r[l[k]] = r[k];
    l[r[k]] = l[k];
}

邻接表

邻接表就是一堆单链表,我们也是用数组模拟的。image-20201021200504845

树是一种特殊的图,所以我们先看如何存储图即可。

绝大部分题都是用邻接表或邻接矩阵来存图。

在算法题中,把无向图看出特殊的有向图。a – b 等价于两条有向图边:a --> b、b --> a 。用邻接矩阵 g[a] [b] 来存储 a–> b。当我们计算最短路径的时候,如果样例中有多条从 a 到 b 的有向边,显然我们只需要用邻接矩阵 g[a] [b] 存储最小的边即可。

邻接矩阵因为要开二维数组,所以更适合存储 稠密图 。因为太占空间,所以算法题中用的不多。

算法图论中,用的最多的是邻接表,因为邻接表适合存储 稀疏图

重边 二或多条 a -> b 边。image-20201119201819131

自环 a -> a 的边。image-20201119201827509

邻接表实现思路

树的直径:任意两点之间的最大路径,即树的直径。因此,未必只有一条树的直径。

邻接表就是每个节点都会有一个单链表。每个点上的单链表就使用来存这个点能走到哪些点。单链表上每个点的次序是随意的。

image-20201021202403986

如下图:

image-20201021202442962

邻接表如下:

image-20201021202548621

若想插入新的边,我们一般选择在单链表的头插法。如下图,h[i] 表示第 i 个节点的单链表的头节点。image-20201021202734397

例如插入一条边:a --> b。

image-20201021202821309

使用到的数组

const int N = 100010, M = N * 2; // 邻接表一般存稀疏图,所以 M 不太大

int h[N]; //
int e[M]; // 存储某个单链表的一个节点的序号
int ne[M]; // 存储:指向某个单链表一个节点的下一个节点的序号
int w[M]; // 存储每条边的权重,当每条边的权重是 1 时,就可以省略这个数组了。
int idx; // 实际存储过程中的数组索引

邻接表操作

初始化
void init() {
    memset(h, -1, sizeof h); // 所有单链表头指针初始化为 -1
    idx = 0;
}
插入

即 在 a 点对应的邻接表里面头插一个 b 节点。如果是无向图那就再 在 b 对应的邻接表里插入 a 节点。

void add(int a, int b) {
   e[idx] = b;
   ne[idx] = h[a];
   h[a] = idx++;
}
// 当每条边的权重可能不是 1 时
void add(int a, int b, int c) {
   e[idx] = b;
   ne[idx] = h[a];
   w[idx] = c;
   h[a] = idx++;
}

图的 DFS

void dfs(int u) {
    st[u] = true; // 标记一下当前节点 u 已经被搜索过了
    
    for (int i = head[u]; i != -1; i = ne[i]) {
        int j = e[i];
        if (st[j] == false) dfs(j);
    }
}

int main()
{
    dfs(1); // 图中的起点是任意的。
}

参考:AcWing

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值