全文目录
- 数组实现的优势
- 单链表
- 初始化
- 头插
- 在下标 `k` 后面插入元素
- 删除下标 `k` 后面的元素
- 遍历
- 双链表
- 初始化
- 插入
- 删除元素
- 遍历
链表在很多语言的标准库里面都有,基本上是通过 Node
节点来链接下一个节点实现的:
struct Node
{
int _data;
Node* _next;
Node* _prev; // 双链表
}
这样的链表使用起来虽然方便,但是有两个缺点:
所以一般,如果数据是整数的话可以使用数组来代替链表,可以大幅度地提升效率。
通过数组实现,需要用到两个数组,一个用来存放数据 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] << ' ';
}
完结散花