摘要:
用数组模拟静态链表。
题目
实现一个单链表,链表初始为空,支持三种操作:
- 向链表头插入一个数;
- 删除第 个插入的数后面的数;
- 在第 个插入的数后插入一个数
现在要对该链表进行 次操作,进行完所有操作后,从头到尾输出整个链表。
注意: 题目中第 个插入的数并不是指当前链表的第 个数。例如操作过程中一共插入了 个数,则按照插入的时间顺序,这 个数依次为:第 个插入的数,第 个插入的数,…第 个插入的数。
输入格式
第一行包含整数 ,表示操作次数。
接下来 行,每行包含一个操作命令,操作命令可能为以下几种:
H x
,表示向链表头插入一个数 。
D k
,表示删除第 个输入的数后面的数(当 为 时,表示删除头结点)。
I k x
,表示在第 个输入的数后面插入一个数 (此操作中 均大于 )。
输出格式
共一行,将整个链表从头到尾输出。
数据范围
输入样例:
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
何为链表
首先什么是链表呢,链表是一堆离散的结点,每个结点存了两个值,一个表示结点的数值,一个是 指针,它指向了下一个结点的位置。链表是一个递归的数据结构(俄罗斯套娃),如果给我们一个链表的头结点,我们可以遍历整个链表。可以认为链表某结点的 指针指向了其后面整个链表。
一个链表结点可以形象地看成:
[value]
[next ]
用数组模拟链表,一定要理解其本质。
数组模拟链表的本质
我们用一个数组 来存储数值,一个数组 来存储指针。一个链表结点分别由同一下标的两个数组元素构成。因为是用数组模拟链表,所以结点的指针用数组下标表示。
从 开始遍历两个数组, 代表第 次插入操作,每一次向链表中新插入数值,下标后移一位,相当于new一个新的结点。但是当删除操作时,下标不会移动。也就是说,删除结点时,结点依然在数组中,只是链表不会指向它。
另外,需要一个下标来表示整个链表的头结点,注意,头结点不一定在 位置(可能会在头结点前面插入新的结点)。
为什么要有头结点下标呢?这样可以很方便地对整个链表遍历,也很方便地在头结点出进行插入和删除操作。
思路
向链表头插入一个数: 即更新头结点。在值域数组新的下标处放入插入的数的值,插入的结点指向了头结点位置,数组下标后移一位方便下次操作。
删除第k个输入的数后面的数: 当 为 时,删除头结点,只需将 移动到 指向的结点; 不为 时,删除第 次插入的结点后面的结点,因为题目中描述“第 次插入”是从 开始计算的,所以此处需要删除下标为 的结点后面的结点。相当于是链表从 结点跳过它后面的结点,指向它后面的后面的结点。
在第k个输入的数后面插入一个数: 题目说明此处 大于 。只需在值域数组新的下标处放入新插入的数的值,在指针域数组新的下标处放入下标为 的结点指向的下一个结点下标,再把下标为 的结点指向新的下标。另外,数组下标后移一位方便下次插入操作。
代码
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;
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() { 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]; else remove(k - 1); } else { cin >> k >> x; add(k - 1, x); } }
for (int i = head; i != -1; i = ne[i]) cout << e[i] << ' '; 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
|
#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; }
|
e[N]
和 ne[N]
数组分别为链表结点的值域数组和指针域数组, head
为链表头结点下标, idx
为两个数组共用的游标,每当需要往链表中插入值时, idx
后移一位,相当于 new
一个新结点。
head
初始化为-1,表示初始状态链表头结点为null
;idx
初始化为0,表示将在两个数组的起始位置添加结点。因为题目的第「k
」个数指的是从 1
起 第 k
个操作数,所以如果我们 idx
初始化为 0
,那么 k
和数组下标的对应关系是 k - 1 == idx
。接着我们注意到算法在使用 k
时是直接当数组下标用的,那是因为在 7
8
处调用函数时,参数已经做了修正(k - 1
)。当然可以让 idx
初始化为 1
,那么 k
和 idx
完全对应,可以直接使用,且调用函数时不需要修正。
new
一个新的结点,让结点的next
指针指向head
,游标后移。
new
一个新的结点,让结点的next
指针指向下标k
的下一位,游标后移。
- 跳过下标为
k
的结点。
- 注意到题干要求当
k
为0的情况。
- 注意题干的第
k
个结点是从1开始计数的,我们算法实现的k
是下标k
。
- 同上。
- 链表的遍历: 从头结点下标开始
i = head
,到null
结点结束i == -1
。因为第一个操作必然是更新头结点(否则其他操作没法进行),接着将头结点指向 -1(null)
,即第一步必然是 head -> null
,随后所有操作都在 head
和 null
之间进行。那么在所有操作结束之后,遍历链表,一定是以 null
结束。
注意到题目说明:
第 个插入的数并不是指当前链表的第 个数。例如操作过程中一共插入了 个数,则按照插入的时间顺序,这 个数依次为:第 个插入的数,第 个插入的数,…第 个插入的数。
那么可以直接初始化 idx
为1,表示从第一个插入的数开始。
见2021-6-29更新的第二版代码。