题解:AcWing 826 单链表

【题目来源】

AcWing:826 单链表 - AcWing题库

【题目描述】

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

  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\))。

【输出】

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

【输入样例】

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

【核心思想】

  1. 问题分析:需要实现一个支持头插、指定位置后插入、指定位置后删除三种操作的单链表,最终输出链表所有元素。关键观察是:题目中的"第 \(k\) 个插入的数"是按插入时间顺序编号的固定标识,而非链表中的动态位置,因此需要用数组模拟链表并维护插入序号到节点下标的映射。

  2. 算法选择

    • 数组模拟链表(静态链表):使用三个数组 e(节点值)、ne(下一个节点下标)、idx(当前可用节点索引)实现,避免动态内存分配和指针操作
    • 插入序号即数组下标:第 \(k\) 个插入的数对应的节点下标就是 \(k-1\)(从 \(0\) 开始),实现了 \(O(1)\) 的定位
  3. 关键步骤

    • 初始化head = -1(空链表),idx = 0(下一个可用节点下标)
    • 头插 H x
      • e[idx] = xne[idx] = headhead = idxidx++
    • 在第 \(k\) 个插入的数后插入 I k x
      • \(k\) 个插入的数的下标为 \(k-1\)
      • e[idx] = xne[idx] = ne[k-1]ne[k-1] = idxidx++
    • 删除第 \(k\) 个插入的数后面的数 D k
      • \(k = 0\)head = ne[head](删除头节点)
      • 否则:ne[k-1] = ne[ne[k-1]](跳过下一个节点)
    • 遍历输出:从 head 开始,沿 ne 数组遍历直到 \(-1\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(M)\),每次操作 \(O(1)\),遍历输出 \(O(n)\)
    • 空间复杂度:\(O(N)\),预分配数组空间
  5. 数组模拟链表的核心思想

    • 下标即指针:用整型数组下标代替指针,ne[i] 存储的是下一个节点的数组下标而非内存地址,避免了指针的复杂性和动态分配开销
    • 插入序号的天然映射:由于节点按插入顺序依次分配下标(\(0, 1, 2, \ldots\)),第 \(k\) 个插入的数的下标就是 \(k-1\),无需额外哈希表即可 \(O(1)\) 定位,这是本题的关键设计
    • 静态分配的优势:一次性预分配数组,操作过程中无需 new/delete,减少了内存碎片和分配开销,适合操作次数确定的竞赛场景
    • 删除的 \(O(1)\) 实现:通过修改前驱节点的 ne 指针跳过待删除节点,无需移动后续元素,体现了链表结构的核心优势
    • 适用于需要频繁插入删除、按固定标识定位节点的场景,是链表在算法竞赛中的标准实现方式

【解题思路】

【算法标签】

链表

【代码详解】

#include <bits/stdc++.h>
using namespace std;

const int N = 100010;  // 定义链表最大容量

// 全局变量定义
int head;      // 头结点下标,初始为-1表示空链表
int e[N];      // e[i]存储节点i的值
int ne[N];     // ne[i]存储节点i的下一个节点下标
int idx;       // 当前可用节点索引,从0开始

/**
 * 初始化链表
 */
void init()
{
    head = -1;  // 头指针初始化为-1
    idx = 0;    // 从0开始分配节点
}

/**
 * 在链表头部插入节点
 * @param x 要插入的值
 */
void add_to_head(int x)
{
    e[idx] = x;     // 存储节点值
    ne[idx] = head; // 新节点指向原头节点
    head = idx;     // 更新头指针
    idx++;          // 分配下一个可用节点
}

/**
 * 在指定位置后插入节点
 * @param k 要插入的位置前驱节点下标
 * @param x 要插入的值
 */
void add(int k, int x)
{
    e[idx] = x;      // 存储节点值
    ne[idx] = ne[k]; // 新节点指向原k节点的下一个节点
    ne[k] = idx;     // k节点指向新节点
    idx++;           // 分配下一个可用节点
}

/**
 * 删除指定位置后的节点
 * @param k 要删除的位置前驱节点下标
 */
void remove(int k)
{
    ne[k] = ne[ne[k]];  // 跳过下一个节点,直接指向下下个节点
}

int main()
{
    int m;  // 操作次数
    cin >> m;

    init();  // 初始化链表

    while (m--)
    {
        char op;  // 操作类型
        int k, x; // 操作参数

        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);   // 删除第k个节点后的节点
        }
        else
        {
            // 指定位置插入操作
            cin >> k >> x;
            add(k - 1, x);       // 在第k个节点后插入
        }
    }

    // 遍历输出链表
    for (int i = head; i != -1; i = ne[i])
        cout << e[i] << " ";
    cout << endl;

    return 0;
}

【运行结果】

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
posted @ 2026-07-02 22:46  团爸讲算法  阅读(13)  评论(0)    收藏  举报