C++:【数据结构】用数组实现的单链表和双向链表

文章介绍了链表作为一种重要的数据结构,与数组的区别,以及如何使用数组来实现单链表和双链表。单链表只有头到尾的指向,而双链表则支持双向查找。文中详细阐述了如何在数组中初始化、插入和删除节点,以及提供了相关代码示例。
摘要由CSDN通过智能技术生成

链表介绍

链表是程序设计中一种十分重要的数据结构。正如它的名字一样,它是一个“链状”的数据结构。如下图所示:
在这里插入图片描述
其中每个节点都可以保存数据,同时每个节点又有一个指针指向下一个节点,来形成链状结构。那么这时候就会有人要说了,这不就是数组吗。其实不然,它和数组各有优缺点。数组就是一串连续的存储单元,而链表不一定是连续的存储单元。数组的优点就是可以很快的访问到某个元素,因为只需要给数组名取下标,就可以访问到某个元素了;但是链表要访问一个元素,需要从头节点开始往后找,越接近链表尾的元素访问的越慢。但是链表可以很方便的在任何位置插入或删除一个元素,但是如果是数组,插入和删除就很墨迹了。
一个标准的链表是用指针来实现的,但本文我们主要来谈谈怎么用数组来实现链表。这两种实现方式大差不差,唯一的一点区别可能就是数组的长度是事先开好的,但用指针实现的链表可以在内存限制之内无限延长。
下面我们来说说怎么用数组来实现单链表和双向链表。

一、单链表

单链表就是,头节点指到末尾的链表。为什么叫单链表呢,因为它只有一个访问方向,即从头到尾。用一张图来说明:
在这里插入图片描述

那么我们下一步就是来实现了。这里我们开两个数组,因为链表的每个节点需要存储值的同时还要指向下一个节点,这两个功能不能用一个数组来实现,所以我们开两个数组,一个用来表示某个节点的值,另一个用来表示某个节点的指针,两个数组下标相同的元素用来表示组合成某个节点(换句话说,因为数组的下标是唯一的,所以我们用下标来表示某个具体的节点)。
此外,我们还要想想怎么表示尾节点,因为数组的下标是从 0 到 n 的,所以我们需要一个不可能的是下标来表示已经到了末尾,这里我们使用 -1 ;再者,我们要用一个变量来表示已经用到了数组的哪个下标,不然就乱套了。
由此,我们就得到了一个空链表,现在我们来初始化它:

//e数组用来存储每个节点的值
//ne(next->e)数组用来表示每个节点的指针
//idx(index)用来表示当前已经用到了数组的哪个位置
//head用来表示头节点
int e[N], ne[N], idx, head;

//初始化链表
void init()
{
    //一开始头节点指向尾节点,来表示链表是空的
    //一开始用了0个元素,所以idx是0
    head = -1, idx = 0;
}

这时候我们就要来考虑一些操作了,我们就来说说基础的插入(分为在表头插入和在任意位置插入)、删除。
首先我们来看看怎么在链表中的两个节点中间插入一个数。首先待插入的两个节点是连起来的:
在这里插入图片描述
现在我们要在 A 节点和 B 节点中间插入一个 C 节点,我们先让 C 指向 B
在这里插入图片描述
然后我们再把 A 节点本来指向 B 的指针指向 C ,这样就成功的插入了。
在这里插入图片描述
注意,一定要先让 C 指向 B ,不能一开始就让 A 指向 C ,因为我们要靠 A 找到 B ,如果我们一开始就让 A 指向 C ,那就找不到 B 了。
现在我们用数组来看看是怎样实现的。假设我们已经有了一个链表:
在这里插入图片描述
其实这个链表就是一直在表头插入得到的,因为我画的链表是表头在左,但是数组是从左到右用的,所以在这张图中,数组最右边的元素(也就是下标最大的元素在表头)。把这张图看懂,就理解了数组实现链表的精髓。这时候 head 的值是 3 ,代表头节点是下标 3 代表的节点。
然后我们开始插入,假如我们要在下标 2 代表的节点下标 1 代表的节点之间插入一个 5 。先按上面所说的,新建一个节点,并让它指向下标 1 代表的节点(因为是下标 2 指向下标 1 ):
在这里插入图片描述
这时候再让下标 2 代表的节点指向这个新插入的节点就好了:
在这里插入图片描述
我们放在代码里来实现:

//在下标 k 的后面插入一个数 x 
void add(int k, int x)
{
    //创建一个新的节点
    e[idx] = x;
    //让新结点的下标指向下一个节点
    ne[idx] = ne[k];
    //让被插入节点指向新建的节点
    //同时让idx + 1表示用到了下一个位置
    ne[k] = idx ++ ;
}

同理给出在表头插入的操作

//在链表头插入一个数 x 
void add_to_head(int x)
{
    e[idx] = x;
    //让当前节点指向头节点代表的节点
    ne[idx] = head;
    //把头节点更新成当前节点
    head = idx ++ ;
}

删除一个节点就简单多了,我们只要让当前节点的指针绕过下一个节点直接指向下一个节点的下一个节点:

//删除下标 k 代表的节点的下一个节点
void remove(int k)
{
    ne[k] = ne[ne[k]];
}

也即:
在这里插入图片描述
因为某个节点只能通过上个节点找到它,现在唯一能找到它的途径被改变了,所以就不可能找到它了,所以就相当于删除了。可能这时候会有人说,可以通过数组下标访问到这个节点,但是我们只是用数组来模拟链表,真正的链表是只能用指针来访问下个节点的,而且真正的链表要及时释放被删除节点的内存,不然就会造成内存浪费。但是我们只是模拟,数组开的也足够大,所以不要在一那些细节。

下面给出一道例题:acwing.单链表
由于题目简单,仅是模板题,这里只给出ac代码:

#include <iostream>

using namespace std;

const int N = 100010;

int head, e[N], ne[N], idx;

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

void add_to_head(int x)
{
    e[idx] = x, ne[idx] = head, head = idx ++ ;
}

void add(int k, int x)
{
    e[idx] = x, ne[idx] = ne[k], ne[k] = idx ++ ;
}

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

int main()
{
    init();
    
    int m;
    cin >> m;
    
    while (m -- )
    {
        char op[2];
        int k, x;
        scanf("%s", op);
        
        if (*op == 'H')
        {
            cin >> x;
            add_to_head(x);
        }
        else if(*op == 'D')
        {
            cin >> k;
            if (k == 0) head = ne[head];
            //由于数组下标是0开始,但数据计数是1开始,所以要用 k - 1
            else remove(k - 1);
        }
        else
        {
            cin >> k >> x;
            add(k - 1, x);
        }
    }
    
    for (int i = head; i != -1; i = ne[i]) cout << e[i] << ' ';
    
    return 0;
}

二、双链表

通过单链表的学习,我们发现了一个致命的缺点:只能顺序查找,不能逆向查找。有时候我们就需要一个可以双向查找,同时又很方便的添加删除元素的数据结构,那么就是双链表。先用一个图来说明:
在这里插入图片描述
可以看到它和单链表的明显不同:

  1. 没有了头节点和尾节点的概念,取而代之的是左边界右边界
  2. 节点间的指针是双向
    有了双向的指针,我们就可以双向查找了。回想一下单链表的实现,现在每个节点包含三个信息:存储的值、左指针、右指针,所以我们就要开三个数组了。另外这里我们偷个懒,不再定义两个变量表示左边界和右边界了,而是直接让下标 0 代表的节点表示左边界,下标 1 代表的节点表示右边界。注意这俩节点是不存储数据的,仅仅作为一个边界的标志。
    有了单链表的知识作为基础,我们双链表的处理其实就是多了处理一个指针的操作。
    首先是我们的初始化链表,因为最开始链表为空,所以左端点和右端点之间没有元素,也就是左端点指向右端点,右端点指向左端点:
//e表示节点存储的值
//l表示左指针,r表示右指针
//idx表示已经用到了数组的哪个位置
int e[N], l[N], r[N], idx;

void init()
{
    //右端点的左指针指向左端点
    l[1] = 0;
    //左端点的右指针指向右端点
    r[0] = 1;
    //0,1已经被占用
    idx = 2;
}

现在来实现插入操作,这里我们实现在下标 k 代表的节点右侧插入一个节点,有了这个操作,我们就可以间接实现在所有地方插入的操作了。
和单链表一样,我们要先建立好新结点指向两个被插入节点的指针,再将两个被插入节点的指针改变到指向新结点:

void insert(int k, int x)
{
    e[idx] = x;
    l[idx] = k, r[idx] = r[k];
    //注意这里一定要先处理k节点右侧的节点
    //因为我们提供的是k节点的信息,所以我们要通过k节点找到k右侧的节点
    //如果先改变k节点的指针,那么我们无法找到k右侧的节点
    l[r[k]] = idx, r[k] = idx ++ ;

有了这个操作,我们就能实现在任何地方插入。比如:

//在k节点左侧插入一个数
insert(l[k], x);

//在最左侧插入一个数,也即在左端点右侧插入一个数
insert(0, x);

//在最右侧插入一个数,也即在右端点左侧插入一个数
insert(l[1], x);

然后来实现删除操作:

//删除k节点
void remove(int k)
{
    //让k节点右侧节点的左指针指向k节点左侧节点
    l[r[k]] = l[k];
    //让k节点左侧节点的右指针指向k节点右侧节点
    r[l[k]] = r[k];
}

这个操作可能有些绕,但其实就是巧妙的将 k 节点越过。实在不明白可以画个图模拟一下。

同样,我们给出一道简单的模板题以及ac代码:acwing.双链表

#include <iostream>

using namespace std;

const int N = 100010;

int e[N], l[N], r[N], idx;

//初始化
void init()
{
    //0是左端点,1是右端点
    r[0] = 1, l[1] = 0;
    idx = 2;
}

//在下标是k的节点右边插入一个数
void insert(int k, int x)
{
    e[idx] = x;
    l[idx] = k, r[idx] = r[k];
    l[r[k]] = idx, r[k] = idx ++ ;
}

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

int main()
{
    init();

    int m;
    cin >> m;

    while (m -- )
    {
        int k, x;
        string op;

        cin >> op;
        if (op == "L")
        {
            cin >> x;
            insert(0, x);
        }
        else if (op == "R")
        {
            cin >> x;
            insert(l[1], x);
        }
        else if (op == "D")
        {
            cin >> k;
            //为什么是k + 1
            //因为初始两个位置被占用,同时数组是以0开始,而计数是以1开始
            //所以正确的位置是 k - 1 + 2 即 k + 1
            remove(k + 1);
        }
        else if (op == "IR")
        {
            cin >> k >> x;
            insert(k + 1, x);
        }
        else
        {
            cin >> k >> x;
            insert(l[k + 1], x);
        }
    }

    for (int i = r[0]; i != 1; i = r[i]) cout << e[i] << ' ';
    cout << endl;

    return 0;
}
  • 7
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值