数组实现单链表和双链表

😀 数组实现的优势

链表在很多语言的标准库里面都有,基本上是通过 Node 节点来链接下一个节点实现的:

struct Node
{
	int _data;
	Node* _next;
	Node* _prev;    // 双链表
}

这样的链表使用起来虽然方便,但是有两个缺点:

  1. 空间更大,因为有内存对齐的存在
  2. 如果有大量的节点,那么会在耗费大量的时间来开辟节点,效率低

所以一般,如果数据是整数的话可以使用数组来代替链表,可以大幅度地提升效率。

🤔 单链表

通过数组实现,需要用到两个数组,一个用来存放数据 e[],一个用来存放下一个节点的下标 ne[],一个用来标记头结点的位置 head,一个用来标记下一个需要用到的位置 index

实现的样子大概是这样:

在这里插入图片描述

😕 初始化

void init()
{
    head = 0;
    index = 1;
}

同样也可以 head = -1, index = 0 来进行初始化,但是这样的话,后面的操作下标都需要 -1 ,所以个人感觉 上一种方式处理起来更加方便。

😕 头插

void add_head(int x)
{
    e[index] = x;
    ne[index] = head;
    head = index;
    index ++;
}

这里需要清楚的是,如果是没有数据的话,那么节点一定是头插的。这样才能保证 head 的更新和顺利找到尾结点。

😕 在下标 k 后面插入元素

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

😕 删除下标 k 后面的元素

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

😕 遍历

for (int i = head; i; i = ne[i])
{
    cout << e[i] << ' ';
}

遍历可能会有一些误导,需要找到初始化的 head 才算是到了尾,

首先这个head指的是链表中头节点的下标,当链表中没有节点时head = 0,但当链表中有值插到头节点的时候,head储存的就是这个值的idx1,通过e[idx1]可以求出这个节点的值。而ne[idx1]就等于head之前的值,即0。如果再在头节点插入一个元素,则head指向这个元素的idx2,而ne[idx2]就等于上一次插入的head值,即idx1。此时,head = idx2,ne[idx2] = idx1,ne[idx1] = 0。若真正理解了这个过程,你的问题就迎刃而解了。拿上述链表举例,在循环链表时,初始的i = head,除非链表为空,否则这个head的值一定不为0,其值应为链表中第一个节点的idx2值,输出e[i]后,有i = ne[i],其含义为i = ne[dix2],由上述论证可知,ne[idx2] = idx1,则i = idx1,满足i != 0,输出e[i]的值。又有i = ne[i],即为i = ne[idx1],可知ne[idx1] = 0,则i = 0,不满足i != 0的循环条件,故循环退出。这个链表的循环依次输出了e[idx2]e[idx1],即链表中的前后两个节点。


😵‍💫 双链表

因为双链表有左右两个指针,所以在实现的时候需要三个数组,一个存储元素 e[],一个存储左节点的下标 l[],一个存储右节点的下标 r[]index 表示用到的下标。

🤨 初始化

初始化数组时,给定左边界和右边界。头结点是从左边界的下一个开始,尾结点就是右边界的前一个。这样可以方便头插和尾插,所以就给定 0为左边界, 1 为右边界, index 从2开始。

因为 index 是从2开始的,所以后面的关于下标的操作都需要加上1

// 初始化
void init()
{
    r[0] = 1;   // 头结点
    l[1] = 0;   // 尾结点
    index = 2;  // 元素的下标从2开始,后面的下标也都需要+1
}

这样初始化可能会觉得很变扭,有点违背我们的思维方式,就会想index能不能从1开始,右边界取2。

答案是不能的,这样会导致index在向后走的时候可能取到右边界,那么对index 进行操作就会导致右边界发生变化。所以要保证 index 不能取到右边界。

🤨 插入

因为是双链表,可以直接找到前一个节点,所以在 k 的左边插入可以表示成在 k 的前一个节点的后面插入,头插就是在左边界的后面插入,尾插就是在右边界的前一个节点后面插入。因此只实现一个插入就好了。

// 在当前节点的后面插入
void insert(int k , int x)
{
    e[index] = x;   
    r[index] = r[k];
    l[index] = k;
    l[r[k]] = index;
    r[k] = index;
    index++;
}

头插:insert(0, x);   // 头插就是在头结点的右边插入

尾插:insert(l[1], x);    // 尾插就是在尾结点的左边插入

在k的左边插入:insert(l[k + 1], x);    // 在左节点的后面插入

在k后插入:insert(k + 1, x);    // 正常插入

🤨 删除元素

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

🤨 遍历

同样的,双链表的遍历只要找到右边界的位置就好了

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

完结散花🌈🌈🌈

在这里插入图片描述

  • 26
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 15
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_featherbrain

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值