有序链表合并的“创新叛逆”写法:段交换
有序链表合并的“叛逆”写法用段交换取代逐结点插入引言有序链表的归并Merge Two Sorted Linked Lists是数据结构课程的经典题目。标准答案通常是尾插法用一个虚拟头结点依次比较两条链表的头部将较小者摘下并挂到新链表尾部。但不久前我在练习时无意写出了一段非常另类的代码——它没有使用前驱指针也没有显式维护尾结点而是通过一种——“交换整个后续链表段”——的诡异操作在十行之内完成了原地归并。本文将剖析这段代码的原理、正确性、复杂度并与常规写法进行对比。常规尾插法复习先回顾一下教科书的标准写法// 带头结点的递增单链表合并尾插法StatusMergeList_Normal(LinkListL1,LinkListL2){LNode*p1L1-next,*p2L2-next;LNode*tailL1;// 尾指针指向结果链表的末尾L1-nextNULL;// 将 L1 头结点独立出来while(p1p2){if(p1-datap2-data){tail-nextp1;p1p1-next;}else{tail-nextp2;p2p2-next;}tailtail-next;}tail-next(p1?p1:p2);free(L2);// 释放 L2 头结点returnOK;}逻辑清晰容易理解没有任何副作用。“叛逆”写法只用一个临时指针下面是我最初写出的代码StatusMergeList_Rebel(LinkListL1,LinkList L2){LNode*p1L1,*p2L2,*tem;while(p1-next!NULLp2-next!NULL){if(p1-next-datap2-next-data){p1p1-next;}else{temp1-next;p1-nextp2-next;p2-nexttem;}}p1-nextp2-next;returnOK;}如果你第一眼觉得它像是“错位版”的归并甚至怀疑它能否跑通——这很正常。让我们逐步拆解。核心机制以“段”为单位交换前置条件L1、L2 均为带头结点的单链表。两条链表均为非递减有序。传入的 L1 和 L2 不是同一个链表否则会产生环。指针职责p1始终指向 L1 中已归并部分的最后一个结点。p2指向 L2 头结点p2-next 是 L2 中剩余部分的首结点。循环逻辑当两条链表都还有剩余结点时情况 Ap1-next-data p2-next-dataL1 当前首结点更小或相等它已经处于正确位置。动作p1 后移一步相当于将它“收纳”进已归并区间。情况 Bp2-next-data 更小此时需要将 L2 的首结点插入到 p1 之后。但代码并没有只摘一个结点而是做了一次“乾坤大挪移”temp1-next;// 暂存 L1 的剩余段p1-nextp2-next;// 将 L2 的剩余段整体接到 p1 之后p2-nexttem;// 将原来的 L1 剩余段整体甩给 L2关键效果·原本在 L2 中的较小结点及其后续整段瞬间进入了 L1 的已归并部分之后。·原本 L1 中较大的那一段被整体转移到了 L2 头结点之后成为下一轮比较的候选。图解一次交换假设当前状态如下p1-next-data 3p2-next-data 2触发 else 分支。交换 p1-next 与 p2-next 后:注意交换后L1 中 p1 的下一个结点是 2正确且后续的 4-8 也自然跟了过来。而 L2 的剩余段变成了 3-5-9依然是有序的。下一轮循环开始时p1 尚未移动但 p1-next-data 已经变成了 2较小者因此会进入情况 Ap1 后移至结点 2。循环继续两条链表的剩余部分仍各自有序不变式始终成立。正确性证明循环不变式初始p1 指向 L1 头结点已归并部分为空p2 指向 L2 头结点。两条链表的剩余段p1-next 与 p2-next均为原链表各自有序。保持每次迭代比较两个剩余段的首结点。若 L1 首结点较小p1 前移将该结点纳入已归并区间。若 L2 首结点较小通过交换指针将 L2 剩余段整体插入到 p1 之后同时将原 L1 剩余段置换到 L2 头结点之后。由于两段各自有序交换后 L1 已归并部分后紧跟的仍是较小者且两条链表的剩余段继续保持有序。无论如何待归并结点总数减 1。终止循环结束后至少一条链表剩余段为空。此时将非空的剩余段可能为 NULL直接接在 p1 之后即完成归并。因此算法正确将 L1 和 L2 的所有结点按非递减序合并到 L1 中。复杂度分析与常规算法横向对比优缺点优点·极简没有显式尾指针用一个临时变量完成所有操作。·思维新颖打破了“逐结点摘取”的惯性展示了链表指针交换的另一种可能性。·原地归并满足 O(1) 额外空间要求。暗坑务必注意·L2 会被“污染”函数返回后L2 头结点的 next 指针可能指向原 L1 的某个中间结点也可能为 NULL。总之L2 不再是一个合法的、可独立使用的链表。调用者应在调用后立即将 L2 视作废弃或者手动 free(L2)。·禁止自引用若 L1 和 L2 指向同一个链表头结点代码会产生环或断链导致死循环或崩溃。·必须带头结点循环判断依赖 p1-next 和 p2-next无头结点的链表将无法正确初始化。结语与思考这段“叛逆”代码是我在写链表练习时无心插柳的结果。它并不适合直接用于生产环境因为它牺牲了代码的可维护性并且带有隐藏的副作用。但在算法学习的过程中这种非主流实现却能极大锻炼我们对指针和链表结构的理解。巧妙与工程稳健之间往往存在权衡。 作为技术博客的读者我希望这篇分析能让你在下次面对链表题目时多一种看待指针的视角。如果你还见过其他“古怪但正确”的链表操作写法欢迎在评论区分享。