数组模拟单链表

通过数组来模拟链表的结构,因为其内存分配是静态的,通常会比使用 new 动态分配内存更快。


全局变量 (Global Variables)

C++

const int N = 100010; // 定义了数组的最大容量

// 核心数组
int h[N], e[N], ne[N], head, idx;
  • e[N] (element array): 这个数组用来存放每个节点存储的e[i] 就表示下标为 i 的节点所存储的数值。可以把它看作是链表节点的 datavalue 字段。
  • 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;
}

这个函数的作用是将链表恢复到最原始的、空无一物的状态。

  1. head = -1;
    • 作用: 将头指针 head 指向 -1
    • 解释: 在这个数组模拟的链表中,我们用数组的下标(0, 1, 2, …)来代表节点的地址。-1 是一个无效的下标,因此它被选作一个特殊的标记,用来表示“没有下一个节点了”或者“这里是空的”。
    • head 初始化为 -1,意思就是“目前链表是空的,没有任何节点”。当你遍历链表时,如果发现一个节点的 next 指针(即 ne 数组的值)是 -1,你就知道已经到达链表的末尾了。
  2. 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 的下标。

  1. e[idx] = x;
    • 作用: 为新节点赋值。
    • 解释: 我们从 idx 获得一个全新的、未被使用的下标。然后,我们将要插入的值 x 存放到这个新下标对应的 e 数组位置上。例如,如果 idx3,那么 e[3] 现在就等于 x。我们有了一个存放了正确值的新节点,只是它还没有被连接到链表中。
  2. ne[idx] = head;
    • 作用: 将新节点的 next 指针指向原来的第一个节点。
    • 解释: 一个新节点要成为新的头节点,那么它的下一个节点就应该是原来的头节点。head 变量里存储的正是原来头节点 A 的下标。所以这行代码让新节点的“指针”指向了 A。现在的状态是:新节点 -> A -> B -> C
  3. head = idx;
    • 作用: 更新头指针 head,让它指向这个新节点。
    • 解释: 链表的头现在已经变了,不再是 A,而是我们刚刚创建的新节点。所以,我们需要更新 head 变量,让它存储新节点的下标 idx。做完这一步,head 就正确地指向了新的链表头部。
  4. idx ++;
    • 作用: 准备下一次插入。
    • 解释: 当前的下标 idx 已经被使用了,所以我们将 idx 加一,确保下一次调用 addint_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 插入到 kP 之间,变成 ... -> 节点k -> 节点X -> 节点P -> ...

  1. e[idx] = x;
    • 作用: 和头插法一样,先为新节点 X 找个窝(下标 idx),并把值 x 存进去。
  2. ne[idx] = ne[k];
    • 作用: 让新节点 Xnext 指针指向 k 原本指向的下一个节点 P
    • 解释: 在插入之前,ne[k] 存储的是节点 P 的下标。我们现在要让新节点 X 的下一个节点成为 P。所以,我们把 ne[k] 的值(也就是 P 的下标)复制给 ne[idx]。这样,X 就成功指向了 P (X -> P)。
  3. ne[k] = idx;
    • 作用: 让节点 knext 指针指向新创建的节点 X
    • 解释: 原本 k 是指向 P 的,现在它们中间要插入 X。所以,我们需要切断 kP 的连接,转而让 k 指向 XX 的下标是 idx,所以我们把 idx 赋值给 ne[k]。这样,k 就成功指向了 X (k -> X)。
    • 顺序的重要性: 注意第2步和第3步的顺序不能颠倒。如果先执行 ne[k] = idx;ne[k] 中原来存储的 P 的下标就会被覆盖丢失,我们就再也找不到节点 P 了。所以必须先用 ne[idx]P 的地址保存下来,然后再修改 ne[k]
  4. 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 的节点。
  • 节点 BA 的下一个节点,也就是我们要删除的目标。它的下标存储在 A 的“指针”里,所以 B 的下标就是 ne[k]
  • 节点 CB 的下一个节点。它的下标存储在 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]];

这行代码是整个操作的核心,我们把它拆解开:

  1. ne[ne[k]]: 这是取值操作。
    • ne[k] 得到节点 B 的下标。
    • ne[ne[k]] 得到节点 Bnext指针所指向的节点 C 的下标。
    • 所以,ne[ne[k]] 的值就是节点 C 的下标
  2. ne[k] = ...: 这是赋值操作。
    • ne[k] 是节点 Anext指针。
    • 整个语句的意思是:将节点 C 的下标赋值给节点 Anext指针

操作后:

执行完这行代码后,节点 Anext 指针不再指向 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题库

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

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

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

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

输入格式

第一行包含整数 M M M,表示操作次数。

接下来 M M M 行,每行包含一个操作命令,操作命令可能为以下几种:

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

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

数据范围

1 ≤ M ≤ 100000 1 \le M \le 100000 1M100000

所有操作保证合法。

输入样例:
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;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值