kizumi_header_banner_img

是先学算法,先刷题,还是先写日记呢?

加载中

文章导读

链表


avatar
RonF02 2026年2月20日 89

链表是一种数据结构,分为单链表和双链表,有STL模板,但这里主要讲一下数组模拟,属于静态链表。可以引申为邻接表,用于存储图或者树,对于图论有一定的作用。

单链表

单链表通过可以通过结构体或者数组模拟,结构体模拟代码如下

struct Node
{
    int val;
    Node *next;
};

但是因为每次调用需要new Node(),而这是一个非常慢的操作,所以一般不会这么干,尤其是算法竞赛中,很容易TLE。

于是我们会需要使用数组来进行,需要使用以下四个变量

  • head:存储头节点的下标
  • idx:存储当前用到了第几个节点
  • e[]:数组,e[i]代表下标为i的节点的值
  • ne[]:数组,ne[i]代表下标为i的节点的下一个指针的坐标

首先需要初始化。受Sunglassman教学线段树时的推荐,使用结构体封装。于是,初始化函数

List()
{
    head = -1, idx = 0;
}

初始化让头节点的坐标为-1,意思是头节点即为结束。这样就实现了初始化一个空的列表
同时把idx赋值为0,便于之后的新增节点的操作。

将 x 插入头节点

void add_to_head(int x)
{
    e[idx] = x; // 赋值
    ne[idx] = head; // 当前的点的next指针指向head
    head = idx; // head指向新节点的位置
    idx ++ ; // 更新idx
}

这样的文字表述可能有些抽象,我们画个图来解释一下这段代码

插入前的链表长这样

head->o->end
idx0-1

蓝色箭头代表了head = 0,红色箭头代表了ne[0] = -1

那么我们在头部插入新节点就是要让列表变成这样

head->new ->o->end
idx10-1

这个时候我们的红色箭头不变,把head更新为1,对标head = idx,并且建立新的指针绿色箭头,即ne[1]更新为原来的头节点,对标ne[idx] = head。随后更新idx的状态。这就是在头节点增加元素的操作

将 x 插入下标为 k 的点后面

void add(int k, int x)
{
    e[idx] = x; // 赋值
    ne[idx] = ne[k]; // 当前的点的 next 指针指向 k 的下一个指针
    ne[k] = idx; // 第k个点的下一个指针更新为当前的idx
    idx ++ ;
}

依旧画图解释

假设插入前的链表长这样

head ->o->o->o ->o ->end
idx1023

然后我们要在2号节点后插入一个元素,此时k = 2

变成这样

head->o->o ->o-> o->o->end
idx10243

红色箭头代表了ne[2]我们插入新元素,把ne[2]更新为了4,也就是idx,同时把ne[4]更新为了3,即原来的ne[2]。这样就对标了代码中的先把ne[idx] = ne[k],再把ne[k] = ne[idx]的操作了。

将下标是 k 的点的后面的点删掉

void remove(int k)
{
    ne[k] = ne[ne[k]]; // 当前的点的ne直接指向下一个点的ne
}

比较好理解,直接将当前的点的ne指向下一个点的ne即可实现跳过下一个点的效果。造成的空间浪费在算法题中可以忽略。

封装代码

struct List{
    // head 头节点下标
    // e[i] 表示节点i的值
    // ne[i] 表示节点i的next指针是多少
    // idx 存储当前已经用到了哪个点
    int head, e[N], ne[N], idx;

    // 初始化
    List()
    {
        head = -1; // 空链表,head指针指向的位置就是end
        idx = 0; // 用了第 0 个点
    }

    // 将 x 插入头节点
    void add_to_head(int x)
    {
        e[idx] = x; // 赋值
        ne[idx] = head; // 当前的点的next指针指向head
        head = idx; // head指向新节点的位置
        idx ++ ; // 更新idx
    }

    // 将 x 插入下标为 k 的点后面
    void add(int k, int x)
    {
        e[idx] = x; // 赋值
        ne[idx] = ne[k]; // 当前的点的 next 指针指向 k 的下一个指针
        ne[k] = idx;
        idx ++ ;
    }

    // 将下标是 k 的点的后面的点删掉
    void remove(int k)
    {
        ne[k] = ne[ne[k]]; // 当前的点的ne直接指向下一个点的ne
    }
    
};

双链表

时隔一年(好吧其实也就只有五六天),今天继续学习双链表了。双链表与单链表类似,只是需要可以反向查找而已。

<- o ->

一个向左,一个向右,即int l[], int r[];

初始化

这里为了偷懒,0号点表示左端点,1号点表示右端点。那么我们初始化的时候就需要构建起0号点和1号点之间的关系,画成图就是这样

0 --- 1

很显然,刚刚会区分左右和数数的幼儿园小朋友都知道,0号点的右边是1号点,1号点的左边是0号点。于是初始化代码就成了

void init()
{
    r[0] = 1, l[1] = 0;
    idx = 2; // 0号点和1号点均被占用
}

插入一个点

我们有两种选择,既可以插入到某个点的左边,又可以插入到某个点的右边。先画个图

原来是酱紫:

ooooo
idx02341
l[idx]/0234
r[idx]2341/

现在呢,假设我们要在三号点和四号点的中间插入五号点,那么就变成了这样

oooooo
idx023541
l[idx]/02354
r[idx]23541/

现在发生了什么变化呢?我们发现,l[4],r[3]都被更新为了新插入的点的idx,新插入的点的 l 变成了原来的左边的点的idx,新插入的点的 r 变成了原来的右边的点的idx。那么我们需要的代码就是如下两个

// 在下标为k的右边插入x
void add_right(int k, int x)
{
    e[idx] = x; // 更新值
    
    r[idx] = r[k]; // 更新的点的r变为原来的点的r
    l[idx] = k; // 更新的点的l变成原来的点的下标
    
    l[r[k]] = idx; // 原的右边的点的l变为现在的idx
    r[k] = idx; // 原来的点的r变为现在的idx
    
    // 顺序很重要,否则会数据污染!
}
// 在下标为k的左边插入x
void add_left(int k, int x)
{
    e[idx] = x; // 更新值
    
    l[idx] = l[k]; // 更新的点的r变为原来的点的r
    r[idx] = k; // 更新的点的l变成原来的点的下标
    
    r[l[k]] = idx; // 原的右边的点的l变为现在的idx
    l[k] = idx; // 原来的点的r变为现在的idx
}
//或者
void add_left(int k, int x)
{
    add_right(l[k], x);
}
// 也就是说只要实现一个即可

顺序务必注意!否则会数据污染!已经踩过一次坑了!

删除

好的,现在我们来看删除操作,依旧先画图。

假设原来这样,我们需要删除三号点

ooooo
idx02341
l[idx]/0234
r[idx]2341/

那这个图就变成了这样:

oooo
idx0241
l[idx]/034
r[idx]231/

对比可以发现,相当于在对三号点没有做任何修改的情况下,分别修改2号点和4号点的 l 和 r , 就能跳过三号点,就相当于从链表中删除了三号点。

那如何修改 l 和 r 呢?只要把 2 号点的 r 改成 4,4号点的 l 改成 2,就可以了。

void remove(int k)
{
    r[l[k]] = r[k];
    l[r[k]] = l[k];
}

封装代码

//const int N = 1e6 + 10;
struct List
{
    int e[N], l[N], r[N], idx;
    List()
    {
        // 0 表示左端点,1 表示右端点
        r[0] = 1, l[1] = 0;
        idx = 2;
    }
    
    // 在下标为k的右边插入x
    void add_right(int k, int x)
    {
        e[idx] = x; // 更新值
        
        r[idx] = r[k]; // 更新的点的r变为原来的点的r
        l[idx] = k; // 更新的点的l变成原来的点的下标
        
        l[r[k]] = idx; // 原的右边的点的l变为现在的idx
        r[k] = idx; // 原来的点的r变为现在的idx
        
        idx ++ ;
    }
    
    void add_left(int k, int x)
    {
        add_right(l[k], x);
    }
    
    void remove(int k)
    {
        r[l[k]] = r[k];
        l[r[k]] = l[k];
    }
    
    void output()
    {
        for (int i = r[0]; i != 1; i = r[i] ) cout << e[i] << ' ';
    }
    
    void debug()
    {
        for (int i = 0; i < idx; i ++ ) cout << e[i] << ' ';
        cout << '\n';
        for (int i = 0; i < idx; i ++ ) cout << l[i] << ' ';
        cout << '\n';
        for (int i = 0; i < idx; i ++ ) cout << r[i] << ' ';
        cout << '\n';
        cout << '\n';
    }
};

好了,链表就此完结~

更新日志

古登堡编辑器的原生code在当前主题(Kizumi)下无法保持正确的排版,因此选用表格来表示链表的图。而表格不能打开定宽功能,同时表格中不能有多余的空格,否则会发生奇怪的错误。



评论(0)

查看评论列表

暂无评论


发表评论

表情 颜文字
插入代码
RonF02的博客

个人信息

avatar

24
文章
2
评论
1
用户