数据结构 - 单链表第二篇:单链表进阶操作

单链表进阶操作详解(指定位置插入 / 删除 / 销毁)

前言

在上一篇《单链表基础操作详解》中,我们掌握了头插 / 尾插 / 头删 / 尾删 / 查找等基础操作,这些操作仅能满足链表的「头尾操作」需求。实际开发中,更多场景需要基于指定节点的插入 / 删除(比如有序链表插入、删除指定值节点)。本文继续沿用「思路 + 步骤 + 踩坑」的讲解方式,拆解单链表进阶操作的核心逻辑,并总结链表操作的通用方法论。

单链表基本功能实现代码仓库完整代码: Luminous/Luminousbegin


一、指定位置后插入(SLInsertAf)
功能说明

在已知节点pos的后方插入新节点,无需二级指针。

cpp 复制代码
void SLInsertAf(LL* pos, DaTy x)
{
    assert(pos); // 检查pos非空(避免传入NULL)
    LL* newnode = SLTBuyNode(x);
    // 核心步骤:先让新节点指向pos的下一个节点,再让pos指向新节点
    newnode->next = pos->next;
    pos->next = newnode;
}
设计思路与插入铁则
  • 时间复杂度:O (1),不需要遍历,是单链表效率最高的指定位置插入。

  • 为什么不用二级指针 :只修改pos节点的next指针,不会改变头指针的指向,所以不需要二级指针。

  • 插入铁则(顺序绝对不能反) :必须先给新节点接好后继,再改前驱的 next。如果反过来写:

    cpp 复制代码
    pos->next = newnode;     // 先改前驱
    newnode->next = pos->next; // 这时候pos->next已经是newnode了,等于自己指向自己

    会直接导致链表断链,后面的节点全部丢失。记住口诀:先连后,再连前

  • 实现三步法

    1. 创建新节点
    2. 新节点的 next 指向 pos 原本的后继
    3. pos 的 next 指向新节点

二、指定位置前插入(SLInsertFr)
功能说明

在已知节点pos的前方插入新节点,需分「pos 是表头」和「pos 非表头」处理。

cpp 复制代码
void SLInsertFr(LL** pphead, LL* pos, DaTy x)
{
    assert(pphead && *pphead); // 检查表头和指针非空
    assert(pos); // 检查pos非空

    if (pos == *pphead) // pos是表头:复用头插法
    {
        PushFront(pphead, x);
    }
    else // pos非表头:找到pos的前驱节点prev
    {
        LL* newnode = SLTBuyNode(x);
        LL* prev = *pphead;
        // 遍历找到pos的前驱(prev->next == pos)
        while (prev->next != pos)
        {
            prev = prev->next;
        }
        // 前驱节点指向新节点,新节点指向pos
        prev->next = newnode;
        newnode->next = pos;
    }
}
设计思路与前驱查找法
  • 为什么需要二级指针:如果 pos 是头节点,前插就等于头插,会修改头指针,所以必须传二级指针。
  • 核心难点:找前驱 单链表没有反向指针,想在 pos 前面插节点,必须从头遍历找到 pos 的前一个节点。遍历终止条件是prev->next == pos------ 当 prev 的下一个节点是 pos 时,prev 就是我们要找的前驱。
  • 代码复用思想 :头节点的情况直接调用已有的PushFront,不用重复写逻辑。重复代码越少,出错概率越低,维护成本也越低。
  • 时间复杂度:O (n),最坏情况要遍历整个链表,这是单链表前插的性能代价。
  • 易错踩坑 :循环条件容易写反成while (prev->next == pos),会导致一次都不执行,prev 停在头节点,直接把链表插断。

三、删除指定节点(SLTErase)
功能说明

删除链表中指定的节点pos,需分「pos 是表头」和「pos 非表头」处理。

cpp 复制代码
void SLTErase(LL** pphead, LL* pos)
{
    assert(pphead && *pphead);
    assert(pos);

    if (pos == *pphead) // pos是表头:复用头删法
    {
        SLDeleteBack(pphead);
    }
    else // pos非表头:找到前驱节点,跳过pos
    {
        LL* prev = *pphead;
        while (prev->next != pos)
        {
            prev = prev->next;
        }
        prev->next = pos->next; // 前驱指向pos的后继
        free(pos); // 释放pos节点
        pos = NULL;
    }
}
设计思路与拓展思考
  • 逻辑对称性:删除指定节点和前插逻辑完全对称,都是「找前驱 → 修改前驱的 next → 处理目标节点」。

  • 拓展:O (1) 删除法(偷天换日) 面试中常考的优化方法:不需要找前驱,把 pos 下一个节点的数据拷贝到 pos 里,然后删除下一个节点。

    cpp 复制代码
    // 偷天换日法(仅作拓展,不推荐工程使用)
    void SLTEraseO1(LL* pos) {
        assert(pos && pos->next);
        LL* next = pos->next;
        pos->a = next->a;    // 后继数据覆盖当前节点
        pos->next = next->next; // 跳过后继
        free(next);
    }

    局限:不能删除尾节点,且本质是删除了后继节点,并非真正删除 pos。工程中推荐使用找前驱的标准写法,逻辑严谨无边界问题。

  • 删除通用步骤:先改链表结构(把节点从链上摘下来),再释放节点内存。顺序不能反。


四、删除指定节点的后继节点(SLTEraseAf)
功能说明

删除pos节点的下一个节点(后继节点),是指定位置删除的简化版。

cpp 复制代码
void SLTEraseAf(LL* pos)
{
    assert(pos && pos->next); // 检查pos非空、后继非空
    LL* del = pos->next; // 保存待删除节点
    pos->next = del->next; // pos指向del的后继
    free(del); // 释放del
    del = NULL;
}
设计思路与常见错误分析
  • 为什么最简单:pos 本身就是待删节点的前驱,不需要遍历找前驱,时间复杂度 O (1)。

  • 新手高频错误

    cpp 复制代码
    // 错误写法:先改指针,再free
    pos->next = pos->next->next;
    free(pos->next); // 此时pos->next已经变了,释放的是错的节点

    后果:删错节点 + 内存泄漏,是链表最经典的 bug 之一。

  • 正确思路先把要删的节点地址存起来,再改链表结构,最后释放存好的地址。永远不要边改指针边释放。


五、销毁链表(SLDestroy)
功能说明

释放链表中所有节点的内存,避免内存泄漏,最终将表头置 NULL。

cpp 复制代码
void SLDestroy(LL** pphead)
{
    assert(pphead);
    LL* cur = *pphead;
    while (cur != NULL)
    {
        LL* next = cur->next; // 先保存下一个节点
        free(cur); // 释放当前节点
        cur = next; // 移动到下一个节点
    }
    *pphead = NULL; // 表头置NULL,避免野指针
}
设计思路与批量删除逻辑
  • 本质:批量版的头删,循环执行「保存下一个 → 释放当前 → 指针后移」。
  • 核心原则 :和单个删除完全一致 ------ 释放前必须保留后续节点的地址。不能边 free 边访问cur->next,free 之后的内存已经不属于你了,再访问就是野指针。
  • 收尾必须置空:释放完所有节点后,一定要把外部的头指针置为 NULL。否则头指针指向已释放的内存,后续误操作会直接崩溃。
  • 思考方法:销毁是链表的「善后操作」,只要用了 malloc,就必须配套销毁函数,程序结束前调用,杜绝内存泄漏。

六、进阶操作测试代码与运行结果
cpp 复制代码
int main()
{
    LL* head = NULL;
    // 初始化链表
    PushBack(&head, 100);
    PushBack(&head, 200);
    PushBack(&head, 300);
    PushBack(&head, 400);
    printf("原链表:");
    Print(head); // 100->200->300->400->NULL

    printf("========== 1. 指定位置后插入 SLInsertAf ==========\n");
    LL* pos3 = SLFin(head, 200);
    if (pos3)
    {
        SLInsertAf(pos3, 250);
        Print(head); // 100->200->250->300->400->NULL
    }

    printf("========== 2. 指定位置前插入 SLInsertFr ==========\n");
    LL* pos4 = SLFin(head, 300);
    if (pos4)
    {
        SLInsertFr(&head, pos4, 280);
        Print(head); // 100->200->250->280->300->400->NULL
    }

    printf("========== 3. 删除指定节点 SLTErase ==========\n");
    LL* pos5 = SLFin(head, 200);
    if (pos5)
    {
        SLTErase(&head, pos5);
        Print(head); // 100->250->280->300->400->NULL
    }
    LL* pos6 = SLFin(head, 100);
    if (pos6)
    {
        SLTErase(&head, pos6);
        Print(head); // 250->280->300->400->NULL
    }

    printf("========== 4. 删除指定节点的后继 SLTEraseAf ==========\n");
    LL* pos7 = SLFin(head, 280);
    if (pos7)
    {
        SLTEraseAf(pos7);
        Print(head); // 250->280->400->NULL
    }

    // 销毁链表
    SLDestroy(&head);
    printf("========== 销毁链表后 ==========\n");
    Print(head); // NULL

    return 0;
}

运行结果:

cpp 复制代码
原链表:100->200->300->400->NULL
========== 1. 指定位置后插入 SLInsertAf ==========
100->200->250->300->400->NULL
========== 2. 指定位置前插入 SLInsertFr ==========
100->200->250->280->300->400->NULL
========== 3. 删除指定节点 SLTErase ==========
100->250->280->300->400->NULL
250->280->300->400->NULL
========== 4. 删除指定节点的后继 SLTEraseAf ==========
250->280->400->NULL
========== 销毁链表后 ==========
NULL

七、进阶篇通用思考方法总结
  1. 插入铁则:先给新节点连好后继,再修改前驱的 next 指针,顺序颠倒必断链。
  2. 删除铁则:先把待删节点地址存到单独变量里,再改链表结构,最后释放。
  3. 前驱思维:往前操作(前插、删当前节点)必须找前驱;往后操作(后插、删后继)直接操作即可。
  4. 代码复用:边界情况能复用头尾操作的,直接调用已有函数,减少重复代码和 bug。
  5. 性能意识:清楚每个操作的时间复杂度,知道单链表适合什么场景、不适合什么场景,才算是真正理解数据结构。
八、总结

单链表的进阶操作,本质是对「指针操作」和「边界思维」的深度考验。没有什么高深的算法,核心就是多练、多踩坑、多总结。掌握完整的单链表操作后,可以进一步学习双向链表、循环链表,或者基于链表实现栈、队列等数据结构。后续我会继续更新数据结构的更多内容(单链表应用、双向链表以及用链表实现的小游戏等),敬请关注!

相关推荐
玖玥拾1 小时前
C/C++ 数据结构(三)链表核心算法
c语言·数据结构·c++·链表
变量未定义~2 小时前
摆放小球 、dp求解组合数、求解组合数2
数据结构·算法
一头老黄牛@2 小时前
飞书 × OpenClaw 接入指南:不用服务器,用长连接把机器人跑起来
数据结构·人工智能·程序人生·算法·决策树·自动化·推荐算法
Zhan8611244 小时前
数据接口的序列号机制与丢包检测:西班牙行情数据IBEX指数实时行情接入笔记
大数据·数据结构·笔记·区块链
玖玥拾12 小时前
C/C++ 基础笔记(十三)继承
c语言·c++·继承
闪闪发亮的小星星12 小时前
开普勒三大定律
笔记
自传.13 小时前
尚硅谷 Vibe Coding|第一章 AI 编程基础理论 学习笔记
笔记·学习·尚硅谷·vibe coding
退休倒计时13 小时前
【每日一题】LeetCode 53. 最大子数组和 TypeScript
数据结构·算法·leetcode·typescript
2601_9618752414 小时前
法考资料2026|全套|资料已整理
数据结构·算法·链表·贪心算法·eclipse·线性回归·动态规划