通过数组来模拟链表的结构,因为其内存分配是静态的,通常会比使用 new
动态分配内存更快。
全局变量 (Global Variables)
C++
const int N = 100010; // 定义了数组的最大容量
// 核心数组
int h[N], e[N], ne[N], head, idx;
e[N]
(element array): 这个数组用来存放每个节点存储的值。e[i]
就表示下标为i
的节点所存储的数值。可以把它看作是链表节点的data
或value
字段。ne[N]
(next element array): 这个数组用来存放每个节点的后继节点的下标。ne[i]
存储的是下标为i
的节点的下一个节点的下标。这完全模拟了链表中的next
指针。例如,如果ne[5] = 8
,就意味着下标为5
的节点指向下标为8
的节点。head
(头指针): 这是一个整型变量,但它的作用是指针。它不存储节点的值,而是存储链表第一个节点的下标。如果链表是空的,head
的值会被设为一个特殊的标记,通常是-1
。idx
(index): 这是一个计数器,用于为新加入的节点分配一个唯一的、未被使用过的下标。每当你需要创建一个新节点时,你就从idx
获取一个下标,然后将idx
自增1
,为下一次分配做准备。
void init()
- 初始化函数
C++
//对链表进行初始化
void init(){
head = -1;
idx = 0;
}
这个函数的作用是将链表恢复到最原始的、空无一物的状态。
head = -1;
- 作用: 将头指针
head
指向-1
。 - 解释: 在这个数组模拟的链表中,我们用数组的下标(0, 1, 2, …)来代表节点的地址。
-1
是一个无效的下标,因此它被选作一个特殊的标记,用来表示“没有下一个节点了”或者“这里是空的”。 - 将
head
初始化为-1
,意思就是“目前链表是空的,没有任何节点”。当你遍历链表时,如果发现一个节点的next
指针(即ne
数组的值)是-1
,你就知道已经到达链表的末尾了。
- 作用: 将头指针
idx = 0;
- 作用: 将下标分配计数器
idx
重置为0
。 - 解释:
idx
总是指向下一个可以用来存储新节点的位置。把它设为0
意味着,我们准备从数组的第0
个位置开始存放第一个新加入的节点。当第一个节点被创建时,它会被放在e[0]
和ne[0]
,然后idx
会变成1
。第二个节点就会被放在e[1]
和ne[1]
,以此类推。
- 作用: 将下标分配计数器
void int_to_head(int x)
- 头插法函数
C++
//将x插入到头节点上
void int_to_head(int x){
e[idx] = x;
ne[idx] = head;
head = idx;
idx ++;
}
这个函数的功能是将一个值为 x
的新节点插入到链表的最前面。我们一步一步来分析:
假设在调用这个函数之前,链表的状态是 A -> B -> C
,并且 head
指向节点 A
的下标。
e[idx] = x;
- 作用: 为新节点赋值。
- 解释: 我们从
idx
获得一个全新的、未被使用的下标。然后,我们将要插入的值x
存放到这个新下标对应的e
数组位置上。例如,如果idx
是3
,那么e[3]
现在就等于x
。我们有了一个存放了正确值的新节点,只是它还没有被连接到链表中。
ne[idx] = head;
- 作用: 将新节点的
next
指针指向原来的第一个节点。 - 解释: 一个新节点要成为新的头节点,那么它的下一个节点就应该是原来的头节点。
head
变量里存储的正是原来头节点A
的下标。所以这行代码让新节点的“指针”指向了A
。现在的状态是:新节点 -> A -> B -> C
。
- 作用: 将新节点的
head = idx;
- 作用: 更新头指针
head
,让它指向这个新节点。 - 解释: 链表的头现在已经变了,不再是
A
,而是我们刚刚创建的新节点。所以,我们需要更新head
变量,让它存储新节点的下标idx
。做完这一步,head
就正确地指向了新的链表头部。
- 作用: 更新头指针
idx ++;
- 作用: 准备下一次插入。
- 解释: 当前的下标
idx
已经被使用了,所以我们将idx
加一,确保下一次调用add
或int_to_head
时会获得一个全新的、不同的下标。
void add(int k, int x)
- 在指定位置后插入函数
C++
//将x插入到下标为k的点的后面
void add(int k, int x){
e[idx] = x;
ne[idx] = ne[k];
ne[k] = idx;
idx ++;
}
这个函数的功能是,在下标为 k
的节点后面,插入一个值为 x
的新节点。
假设在调用这个函数之前,链表的一部分是 ... -> 节点k -> 节点P -> ...
。也就是说,ne[k]
的值是节点 P
的下标。我们的目标是把新节点 X
插入到 k
和 P
之间,变成 ... -> 节点k -> 节点X -> 节点P -> ...
。
e[idx] = x;
- 作用: 和头插法一样,先为新节点
X
找个窝(下标idx
),并把值x
存进去。
- 作用: 和头插法一样,先为新节点
ne[idx] = ne[k];
- 作用: 让新节点
X
的next
指针指向k
原本指向的下一个节点P
。 - 解释: 在插入之前,
ne[k]
存储的是节点P
的下标。我们现在要让新节点X
的下一个节点成为P
。所以,我们把ne[k]
的值(也就是P
的下标)复制给ne[idx]
。这样,X
就成功指向了P
(X -> P
)。
- 作用: 让新节点
ne[k] = idx;
- 作用: 让节点
k
的next
指针指向新创建的节点X
。 - 解释: 原本
k
是指向P
的,现在它们中间要插入X
。所以,我们需要切断k
到P
的连接,转而让k
指向X
。X
的下标是idx
,所以我们把idx
赋值给ne[k]
。这样,k
就成功指向了X
(k -> X
)。 - 顺序的重要性: 注意第2步和第3步的顺序不能颠倒。如果先执行
ne[k] = idx;
,ne[k]
中原来存储的P
的下标就会被覆盖丢失,我们就再也找不到节点P
了。所以必须先用ne[idx]
把P
的地址保存下来,然后再修改ne[k]
。
- 作用: 让节点
idx ++;
- 作用: 和之前一样,为下一次插入操作准备一个新的可用下标。
通过这几步操作,链表的连接关系就从 k -> P
变成了 k -> X -> P
,成功地完成了插入。虽然节点的物理下标(idx
)可能是乱序的,但这完全不影响链表的逻辑顺序,因为我们总是通过 ne
数组这个“指针”来遍历和访问链表的。
C++ 删除下标为 k
的节点的下一个节点函数
void remove(int k){
ne[k] = ne[ne[k]];
}
函数目标
这个函数的目的不是删除下标为 k
的节点,而是删除下标为 k
的节点的下一个节点。
图解说明
假设我们链表的某一部分是 ... -> A -> B -> C -> ...
,我们的目标是删除节点 B
。
在这个场景下:
- 节点
A
就是我们函数参数中下标为k
的节点。 - 节点
B
是A
的下一个节点,也就是我们要删除的目标。它的下标存储在A
的“指针”里,所以B
的下标就是ne[k]
。 - 节点
C
是B
的下一个节点。它的下标存储在B
的“指针”里,所以C
的下标就是ne[B的下标]
,也就是ne[ne[k]]
。
操作前:
整个连接关系是靠 ne
数组(我们的“指针”)来维系的。
k ne[k] ne[ne[k]]
+-------+ +-------+ +-------+
... ->| 节点A | --ne[k]--> | 节点B | --ne[B]--> | 节点C | -> ...
+-------+ +-------+ +-------+
ne[k]
的值是节点B
的下标。ne[ B的下标 ]
(即ne[ne[k]]
) 的值是节点C
的下标。
执行 ne[k] = ne[ne[k]];
这行代码是整个操作的核心,我们把它拆解开:
ne[ne[k]]
: 这是取值操作。ne[k]
得到节点B
的下标。ne[ne[k]]
得到节点B
的next
指针所指向的节点C
的下标。- 所以,
ne[ne[k]]
的值就是节点C
的下标。
ne[k] = ...
: 这是赋值操作。ne[k]
是节点A
的next
指针。- 整个语句的意思是:将节点
C
的下标赋值给节点A
的next
指针。
操作后:
执行完这行代码后,节点 A
的 next
指针不再指向 B
,而是直接指向了 C
。
k ne[k] ne[ne[k]]
+-------+ +-------+ +-------+
... ->| 节点A | \ | 节点B | | 节点C | -> ...
| | \ +-------+ +-------+
+-------+ \ ^
| \____________________________|
|
ne[k] 的值现在是节点 C 的下标
节点 B
依然存在于我们的 e[]
和 ne[]
数组中,但是从 head
开始遍历的这条主链路上,已经没有任何一个节点的next
指针指向它了。它被完美地“绕过”和“跳过”了,因此在逻辑上,它已经被从链表中删除了。
总结
remove(k)
函数通过一步指针(下标)的重新赋值,实现了 O(1) 时间复杂度的删除操作。它并没有真正地从内存中擦除数据,而是通过修改前一个节点的“指针”,将要删除的节点从链表的逻辑结构中“断开”,从而达到了删除的效果。这正是链表数据结构高效插入和删除的优势所在。
[acwing]:826. 单链表 - AcWing题库
实现一个单链表,链表初始为空,支持三种操作:
- 向链表头插入一个数;
- 删除第 k个插入的数后面的一个数;
- 在第 k 个插入的数后插入一个数。
现在要对该链表进行 M 次操作,进行完所有操作后,从头到尾输出整个链表。
注意:题目中第 k个插入的数并不是指当前链表的第 k个数。例如操作过程中一共插入了 n个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。
输入格式
第一行包含整数 M M M,表示操作次数。
接下来 M M M 行,每行包含一个操作命令,操作命令可能为以下几种:
H x
,表示向链表头插入一个数 x x x。D k
,表示删除第 k 个插入的数后面的数(当 k 为 0 时,表示删除头结点)。I k x
,表示在第 k个插入的数后面插入一个数 x x x(此操作中 k均大于 0)。
输出格式
共一行,将整个链表从头到尾输出。
数据范围
1 ≤ M ≤ 100000 1 \le M \le 100000 1≤M≤100000
所有操作保证合法。
输入样例:
10
H 9
I 1 1
D 1
D 0
H 6
I 3 6
I 4 5
I 4 5
I 3 4
D 6
输出样例:
6 4 6 5
C++代码:
#include <iostream>
using namespace std;
const int N = 100010;
int head, e[N], ne[N], idx;
int M;
void init() {
head = -1;
idx = 0;
}
void head_insert(int x) {
e[idx] = x;
ne[idx] = head;
head = idx;
idx++;
}
void add(int k, int x) {
e[idx] = x;
ne[idx] = ne[k];
ne[k] = idx;
idx++;
}
void remove(int k) {
ne[k] = ne[ne[k]];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(NULL);
cin >> M;
init();
while (M--) {
char op;
cin >> op;
if (op == 'H') {
int x;
cin >> x;
head_insert(x);
} else if (op == 'I') {
int k, x;
cin >> k >> x;
add(k - 1, x);
} else {
int k;
cin >> k;
if (k == 0) {
head = ne[head];
} else {
remove(k - 1);
}
}
}
for (int i = head; i != -1; i = ne[i]) {
cout << e[i] << " ";
}
cout << endl;
return 0;
}