数据结构 - 单链表第二篇:单链表进阶操作
单链表进阶操作详解指定位置插入 / 删除 / 销毁前言在上一篇《单链表基础操作详解》中我们掌握了头插 / 尾插 / 头删 / 尾删 / 查找等基础操作这些操作仅能满足链表的「头尾操作」需求。实际开发中更多场景需要基于指定节点的插入 / 删除比如有序链表插入、删除指定值节点。本文继续沿用「思路 步骤 踩坑」的讲解方式拆解单链表进阶操作的核心逻辑并总结链表操作的通用方法论。单链表基本功能实现代码仓库完整代码Luminous/Luminousbegin一、指定位置后插入SLInsertAf功能说明在已知节点pos的后方插入新节点无需二级指针。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。如果反过来写pos-next newnode; // 先改前驱 newnode-next pos-next; // 这时候pos-next已经是newnode了等于自己指向自己会直接导致链表断链后面的节点全部丢失。记住口诀先连后再连前。实现三步法创建新节点新节点的 next 指向 pos 原本的后继pos 的 next 指向新节点二、指定位置前插入SLInsertFr功能说明在已知节点pos的前方插入新节点需分「pos 是表头」和「pos 非表头」处理。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 非表头」处理。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 里然后删除下一个节点。// 偷天换日法仅作拓展不推荐工程使用 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节点的下一个节点后继节点是指定位置删除的简化版。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)。新手高频错误// 错误写法先改指针再free pos-next pos-next-next; free(pos-next); // 此时pos-next已经变了释放的是错的节点后果删错节点 内存泄漏是链表最经典的 bug 之一。正确思路先把要删的节点地址存起来再改链表结构最后释放存好的地址。永远不要边改指针边释放。五、销毁链表SLDestroy功能说明释放链表中所有节点的内存避免内存泄漏最终将表头置 NULL。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-nextfree 之后的内存已经不属于你了再访问就是野指针。收尾必须置空释放完所有节点后一定要把外部的头指针置为 NULL。否则头指针指向已释放的内存后续误操作会直接崩溃。思考方法销毁是链表的「善后操作」只要用了 malloc就必须配套销毁函数程序结束前调用杜绝内存泄漏。六、进阶操作测试代码与运行结果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; }运行结果原链表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七、进阶篇通用思考方法总结插入铁则先给新节点连好后继再修改前驱的 next 指针顺序颠倒必断链。删除铁则先把待删节点地址存到单独变量里再改链表结构最后释放。前驱思维往前操作前插、删当前节点必须找前驱往后操作后插、删后继直接操作即可。代码复用边界情况能复用头尾操作的直接调用已有函数减少重复代码和 bug。性能意识清楚每个操作的时间复杂度知道单链表适合什么场景、不适合什么场景才算是真正理解数据结构。八、总结单链表的进阶操作本质是对「指针操作」和「边界思维」的深度考验。没有什么高深的算法核心就是多练、多踩坑、多总结。掌握完整的单链表操作后可以进一步学习双向链表、循环链表或者基于链表实现栈、队列等数据结构。后续我会继续更新数据结构的更多内容单链表应用、双向链表以及用链表实现的小游戏等敬请关注