VIRTUALS

the virtual labs for the virtuals

0%

【AcWing算法基础】第二讲-数据结构-单链表 AcWing 826. 单链表

摘要:
用数组模拟静态链表。

题目

实现一个单链表,链表初始为空,支持三种操作:

  1. 向链表头插入一个数;
  2. 删除第 $k$ 个插入的数后面的数;
  3. 在第 $k$ 个插入的数后插入一个数

现在要对该链表进行 $M$ 次操作,进行完所有操作后,从头到尾输出整个链表。

注意: 题目中第 $k$ 个插入的数并不是指当前链表的第 $k$ 个数。例如操作过程中一共插入了 $n$ 个数,则按照插入的时间顺序,这 $n$ 个数依次为:第 $1$ 个插入的数,第 $2$ 个插入的数,…第 $n$ 个插入的数。

输入格式
第一行包含整数 $M$,表示操作次数。
接下来 $M$ 行,每行包含一个操作命令,操作命令可能为以下几种:

  1. H x,表示向链表头插入一个数 $x$。
  2. D k,表示删除第 $k$ 个输入的数后面的数(当 $k$ 为 $0$ 时,表示删除头结点)。
  3. I k x,表示在第 $k$ 个输入的数后面插入一个数 $x$(此操作中 $k$ 均大于 $0$ )。

输出格式
共一行,将整个链表从头到尾输出。

数据范围

  • $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


何为链表

首先什么是链表呢,链表是一堆离散的结点,每个结点存了两个值,一个表示结点的数值,一个是 $next$ 指针,它指向了下一个结点的位置。链表是一个递归的数据结构(俄罗斯套娃),如果给我们一个链表的头结点,我们可以遍历整个链表。可以认为链表某结点的 $next$ 指针指向了其后面整个链表。
一个链表结点可以形象地看成:
[value]
[next ]
用数组模拟链表,一定要理解其本质。

数组模拟链表的本质

我们用一个数组 $A$ 来存储数值,一个数组 $B$ 来存储指针。一个链表结点分别由同一下标的两个数组元素构成。因为是用数组模拟链表,所以结点的指针用数组下标表示。
从 $0$ 开始遍历两个数组,$0$ 代表第 $0$ 次插入操作,每一次向链表中新插入数值,下标后移一位,相当于new一个新的结点。但是当删除操作时,下标不会移动。也就是说,删除结点时,结点依然在数组中,只是链表不会指向它。
另外,需要一个下标来表示整个链表的头结点,注意,头结点不一定在 $0$ 位置(可能会在头结点前面插入新的结点)。
为什么要有头结点下标呢?这样可以很方便地对整个链表遍历,也很方便地在头结点出进行插入和删除操作。

思路

向链表头插入一个数: 即更新头结点。在值域数组新的下标处放入插入的数的值,插入的结点指向了头结点位置,数组下标后移一位方便下次操作。
删除第k个输入的数后面的数: 当 $k$ 为 $0$ 时,删除头结点,只需将 $head$ 移动到 $head$ 指向的结点;$k$ 不为 $0$ 时,删除第 $k$ 次插入的结点后面的结点,因为题目中描述“第 $k$ 次插入”是从 $1$ 开始计算的,所以此处需要删除下标为 $k-1$ 的结点后面的结点。相当于是链表从 $k-1$ 结点跳过它后面的结点,指向它后面的后面的结点。
在第k个输入的数后面插入一个数: 题目说明此处 $k$ 大于 $0$。只需在值域数组新的下标处放入新插入的数的值,在指针域数组新的下标处放入下标为 $k-1$ 的结点指向的下一个结点下标,再把下标为 $k-1$ 的结点指向新的下标。另外,数组下标后移一位方便下次插入操作。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <iostream>

using namespace std;

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

// 初始化
void init()
{
head = -1; // 2
idx = 0;
}

// 将x插到头结点
void add_to_head(int x)
{
e[idx] = x, ne[idx] = head, head = idx ++ ; // 3
}

// 将x插到下标是k的点后面
void add(int k, int x)
{
e[idx] = x, ne[idx] = ne[k], ne[k] = idx ++ ; // 4
}

// 将下标是k的点后面的点删掉
void remove(int k)
{
ne[k] = ne[ne[k]]; // 5
}

int main()
{
int m;
cin >> m;
init();
while (m -- )
{
int k, x;
char op;

cin >> op;
if (op == 'H')
{
cin >> x;
add_to_head(x);
}
else if (op == 'D')
{
cin >> k;
if (!k) head = ne[head]; // 6
else remove(k - 1); // 7
}
else
{
cin >> k >> x;
add(k - 1, x); // 8
}
}

for (int i = head; i != -1; i = ne[i]) cout << e[i] << ' '; // 9
cout << endl;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/*
* 2021-6-28 23:59:07
* author: zxy
*/
#include <bits/stdc++.h>

using namespace std;

constexpr int N = 100010;
int a[N], ne[N];
int idx = 1, head = -1;

inline void add_to_k(int k, int x)
{
a[idx] = x;
ne[idx] = ne[k];
ne[k] = idx;
++idx;
}

inline void delete_k(int k)
{
if (!k) head = ne[head];
else ne[k] = ne[ne[k]];
}

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

int main()
{
int m;
cin >> m;
for (; m--;) {
char op;
cin >> op;
if (op == 'H') {
int x;
cin >> x;
add_to_head(x);
}
else if (op == 'D') {
int k;
cin >> k;
delete_k(k);
}
else {
int k, x;
cin >> k >> x;
add_to_k(k, x);
}
}
for (; head != -1; head = ne[head]) cout << a[head] << ' ';
return 0;
}
  1. e[N]ne[N] 数组分别为链表结点的值域数组和指针域数组, head 为链表头结点下标, idx 为两个数组共用的游标,每当需要往链表中插入值时, idx 后移一位,相当于 new 一个新结点。
  2. head初始化为-1,表示初始状态链表头结点为nullidx初始化为0,表示将在两个数组的起始位置添加结点。因为题目的第「k」个数指的是从 1 起 第 k 个操作数,所以如果我们 idx 初始化为 0 ,那么 k 和数组下标的对应关系是 k - 1 == idx 。接着我们注意到算法在使用 k 时是直接当数组下标用的,那是因为在 7 8 处调用函数时,参数已经做了修正(k - 1)。当然可以让 idx 初始化为 1 ,那么 kidx 完全对应,可以直接使用,且调用函数时不需要修正。
  3. new一个新的结点,让结点的next指针指向head,游标后移。
  4. new一个新的结点,让结点的next指针指向下标k的下一位,游标后移。
  5. 跳过下标为k的结点。
  6. 注意到题干要求当k为0的情况。
  7. 注意题干的第k个结点是从1开始计数的,我们算法实现的k是下标k
  8. 同上。
  9. 链表的遍历: 从头结点下标开始i = head,到null结点结束i == -1。因为第一个操作必然是更新头结点(否则其他操作没法进行),接着将头结点指向 -1(null) ,即第一步必然是 head -> null ,随后所有操作都在 headnull 之间进行。那么在所有操作结束之后,遍历链表,一定是以 null 结束。

注意到题目说明:
第 $k$ 个插入的数并不是指当前链表的第 $k$ 个数。例如操作过程中一共插入了 $n$ 个数,则按照插入的时间顺序,这 $n$ 个数依次为:第 $1$ 个插入的数,第 $2$ 个插入的数,…第 $n$ 个插入的数。
那么可以直接初始化 idx 为1,表示从第一个插入的数开始。
见2021-6-29更新的第二版代码。

原题链接: Acwing 826. 单链表