有序链表合并详解:C语言双指针原地归并实现

发布时间:2026/9/30 3:06:35

有序链表合并详解:C语言双指针原地归并实现 两副有序的扑克牌合在一起还要保持有序你会怎么做大多数人会从两叠牌的最上面各取一张比对小的拿下来放新牌堆。这道链表的合并习题本质上就是把这个动作翻译成指针操作。很多数据结构教材会把两个有序链表序列的合并放在线性表章节的习题里看起来平平无奇但它几乎涵盖了单链表操作的所以基本功指针移动、边界判断、头结点处理、空间复用。不管你是期末备考、考研刷题还是想补一补基本功这篇文章都适合你。我会用 C 语言给出完整可运行的解法拆解每一步为什么这么做然后把我自己踩过的坑和调试方法一并分享出来。1. 先看清题目到底要你做什么1.1 题目考的是归并而不是排序先明确习题2.5这类题目的经典表述有两个按值非递减排列的有序单链表La和Lb要求将它们合并成一个新的有序单链表LcLc仍然按值非递减排列。关键约束是——要求利用原表的结点空间不另设新的结点。换句话说你不能 new 出一堆新节点把数据搬过去而是在原有的节点之间改改指针把两条链串成一条有序的链。你可能会想把两条链的数据放进数组排序一遍再建一条新链不也挺简单能跑但这不是题目想考察的。数据结构习题的意图从来不是用任何方法解决问题而是用最贴合这个结构的办法解决问题。如果借助数组时间复杂度是 O((mn)log(mn))空间复杂度 O(mn)完全没有体现链表的优势。这题真正想让你掌握的是归并思想——两个已经有序的序列怎样用 O(mn) 时间合并成有序序列。归并排序之所以叫归并核心合并步骤用的正是这个思路。要注意非递减这个词。很多教材写的是非递减而不是递增意味着允许相等元素连续出现。这个细节会影响你合并时相等元素的处理方式也涉及稳定性问题后面代码部分我会细说。1.2 为什么一定用链表而不是数组数组也能实现归并那教材为什么非要用链表因为链表有数组不具备的两个特性一是插入不需要移动元素二是不需要预先知道总长度。对于合并这个问题数组版的归并你通常得开一块临时空间最后再把结果拷回去而链表版的合并只需要改一连串next指针不需要额外的存储空间这是一种物理意义上的原地合并。另一个隐藏考点是头结点。链表分带头结点和不带头结点两种很多初学的人在这上面翻车。带头结点的链表有一个哑结点dummy node作为哨兵好处是无论是插入第一个元素还是删除第一个元素操作逻辑都不用单独写特殊情况而不带头结点的链表头指针本身就是一个真实的存储数据的节点一旦这个节点被移走你要记得更新头指针。本题目如果你用带头结点的写法代码会简洁很多。这也是我推荐的写法。2. 核心算法思路与为什么这样做2.1 双指针遍历从两副有序的牌说起想象你手上有两堆已经按从小到大排好的牌每堆的最上面是最小的牌。你想得到一叠从小到大的牌做法就是每次只看两堆最上面的那张牌取较小的一张放到新牌堆的底部然后继续。这个每次只看两堆最上面的操作翻译到链表里就是两个指针分别指向两条链表的当前节点。一开始pa指向La的第一个有效节点pb指向Lb的第一个有效节点。每次比较pa-data和pb-data谁小就把谁接到结果链表的尾部然后让对应的指针向后移一步。一轮循环下来当一个指针变成NULL说明这条链表已经遍历完了另一条链表剩下的节点直接接到结果后面即可。这就是双指针法也叫二路归并的合并阶段。它之所以高效是因为每次比较只需要 O(1) 时间而且每个节点最多被移动一次。2.2 为什么可以原地合并不需要新建节点如果说双指针是主心骨那么复用节点就是这题最优雅的地方。因为两个链表原本就是有序的我们做的只是把节点从两条链上解下来重新串起来。每个节点的next指针可以被覆盖节点本身还在所以不需要 malloc 任何新空间。举个例子La {1, 3, 5}Lb {2, 4, 6}。合并开始pa指向1所在的节点pb指向2所在的节点。因为1 2我们让结果链表的尾部直接指向1这个节点然后pa指向3。下一次比较3和22更小就把2节点接过来pb指向4。整个过程没有任何节点被复制或新建只是指针在不断重连。这样做的好处不只是省内存。面试里如果你写的解法是新建了 mn 个节点面试官往往会追问一句能不能不建新节点这题就是要训练你用指针操作去缝合两条链的能力。我个人觉得这种在原有的基础上重排关系的思路比单纯地造新数据要难得多但也值钱得多。3. 完整代码实现与逐段讲解3.1 结构体定义与基础工具函数先定义单链表的节点结构这是整个实验的基础。这里采用最常见的定义方式#include stdio.h #include stdlib.h typedef struct LNode { int data; struct LNode *next; } LNode, *LinkList;LinkList本质上是LNode *但用这个别名能明确表达它是一条链表的头指针这层语义。为了测试我们通常还需要一个创建带头结点链表并插入数据的函数。下面我提供一个简单的尾插法把数组中的元素逐一追加到链表尾部void CreateList(LinkList L, int arr[], int n) { L (LinkList)malloc(sizeof(LNode)); // 创建头结点 L-next NULL; LNode *tail L; for (int i 0; i n; i) { LNode *s (LNode *)malloc(sizeof(LNode)); s-data arr[i]; s-next NULL; tail-next s; tail s; } }这里的C风格引用写法LinkList L是为了让函数内部修改头指针能传回外层。如果你在纯 C 环境下编译需要改成二级指针LinkList *L后面我会提到这点的注意事项。还需要一个打印函数方便我们调试时随时查看链表内容void PrintList(LinkList L) { LNode *p L-next; while (p ! NULL) { printf(%d , p-data); p p-next; } printf(\n); }3.2 合并函数详解带头结点版本下面进入核心部分。我直接给出合并函数然后逐行拆解为什么这么写void MergeList(LinkList La, LinkList Lb, LinkList Lc) { LNode *pa La-next; // 指向 La 的第一个有效节点 LNode *pb Lb-next; // 指向 Lb 的第一个有效节点 Lc La; // 复用 La 的头结点作为 Lc 的头结点 LNode *pc Lc; // pc 始终指向结果链表的当前尾部 while (pa ! NULL pb ! NULL) { if (pa-data pb-data) { pc-next pa; // 把 pa 指向的节点接到结果链尾部 pc pa; // 更新尾部指针为 pa 所在的节点 pa pa-next; // pa 向后移动继续比较 } else { pc-next pb; pc pb; pb pb-next; } } // 循环结束后最多只剩一条链表还有剩余节点 pc-next (pa ! NULL) ? pa : pb; free(Lb); // 释放 Lb 的头结点 }这段代码最核心的地方在于Lc La这一步。因为题目允许复用原节点我们直接把La的头结点征用为结果链表的头结点这样就不用再为了头结点单独 malloc 一块内存。很多初学者会另写Lc (LinkList)malloc(sizeof(LNode))那也可以但就多了一次分配和一次释放没必要。pc Lc以后pc始终指向当前结果链表的最后一个节点。为什么pa和pb的一大段比较逻辑里每一步都是先接节点、再更新尾部、再后移指针因为顺序不能乱。如果你先把pa pa-next那你就丢失了当前节点没法把它接到结果链表上了。如果你先把pc pa再把pa pa-next也能工作但要注意pc-next还没设置就移动了pc之后反而容易漏接。我建议初学者统一按接、移、走三步走第一步让前一个尾部节点指向当前节点第二步让pc跳到当前节点第三步让原链指针往后走。循环结束以后为什么一句话就能接完剩余部分因为pc-next pa ? pa : pb。如果pa不是NULL说明pb已经走完La剩下的所有节点都已经按序连好了直接整串接过去即可反之则接pb。这一步同时处理了三类情况pa为空、pb为空、两者都为空此时pc-next NULL正好。最后别忘了free(Lb)Lb的头结点已经没有任何用处了不释放会造成内存泄漏。3.3 边界条件与特殊情况处理边界条件往往是代码写对的关键。我整理了这题最常见的四种边界情形以及代码里是怎么自动兜住的第一种两条链表都为空。此时pa和pb都是NULL循环不会执行pc-next (pa ! NULL) ? pa : pb得到NULL。合并结果就是一个空链表正确。第二种一条链表为空另一条非空。比如La为空Lb {2, 5}。pa NULL循环不进入直接pc-next pb结果就是{2, 5}。这正好是合并的最简形态。第三种两条链表等长且所有元素交替更小。比如{1, 3, 5}和{2, 4, 6}。while 循环会完整走完最后pa和pb同时变为NULLpc-next接到空指针。正确。第四种一条链表很短另一条很长。比如{1}和{2, 3, 4}。循环第一次比较后pa变成NULL循环退出直接接上pb所在的整条链。正确。我刚才代码里用的是pa-data pb-data也就是相等时把pa的节点先接进来。这样写的好处是保持了稳定性原来在La中先出现的元素合并后依然在Lb的同值元素前面。如果题目没有明确要求稳定性也能跑但面试时能说出相等时优先取第一个链表的节点可以维持稳定性会是不错的加分项。4. 复杂度分析与测试用例验证4.1 时间与空间复杂度推导先说时间复杂度。设La有 m 个节点Lb有 n 个节点。while 循环的每一轮都会让pa或pb其中一个向后移动一步把它们想象成两条队伍每轮消耗一个节点。最坏情况下要一直比到两条链都走完才结束循环执行 m n 轮。循环结束后剩余节点的连接操作是 O(1) 的。所以总时间复杂度是 O(m n)。这其实是最好的结果了。因为要合并两个有序序列至少得把两个序列都看一遍才能确定全局顺序不可能低于 O(m n)。如果你看到有人声称 O(1) 时间合并完那一定是在玩文字游戏要么他已经提前知道了两个序列的某些特殊关系要么他把合并和连接混为一谈。再说空间复杂度。因为我们没有新建任何节点只是复用了La和Lb的节点额外只用了几个指针变量所以额外空间是 O(1)。如果你自己新建一条结果链那空间复杂度就是 O(m n)。4.2 构造测试数据验证空说无凭我准备了三组测试数据读者可以自己复制代码跑一遍看看。第一组是最常规的情况La {1, 2, 4}Lb {1, 3, 4}。输出应当是1 1 2 3 4 4。注意两个链表里都有1和4这能验证相等节点是否都保留下来。第二组验证一条链更长La {1, 3}Lb {2, 4, 5, 6, 7}。输出应当是1 2 3 4 5 6 7。这个用例主要检查循环结束后剩余节点是否正确整串接入。第三组验证边界La {}Lb {0}。输出应当是0。这个用例专门验证空链表的情形。测试代码大概是这样的int main() { LinkList A, B, C; int a[] {1, 2, 4}; int b[] {1, 3, 4}; CreateList(A, a, 3); CreateList(B, b, 3); printf(La: ); PrintList(A); printf(Lb: ); PrintList(B); MergeList(A, B, C); printf(Lc: ); PrintList(C); return 0; }我实际跑过很多次这段代码输出是稳定的。要注意的是A和B在调用MergeList之后就不能再单独访问了因为它们的节点已经混入C的链表结构中。这不是 bug正式这种指针转移才实现了原地合并。5. 实操中常见的坑与排查技巧5.1 空指针访问与断链问题我见过太多人在这道题的代码上栽跟头总结下来高频问题就三类。第一类是空指针访问。比如你在循环里写pc-next pa-next然后pa pa-next最后发现输出不全。原因通常是你本意是让结果链接上pa当前节点结果你把pa-next接过去了跳过了pa本身后续指针错位还可能把NULL解引用。第二类是断链。什么叫断链就是你用pc接上了pa随后pa后移但之前pa-next指向的那个节点没有任何指针指向它了。在一般的比较逻辑里这不会出问题因为比较的双方总有一方还在被另一个指针pa或pb持有。真正容易断链的时候是循环结束后的pc-next赋值有人会写if (pa) pc-next pa; if (pb) pc-next pb;这其实也行但注意这是两个独立的 if如果前面已经接上了pa后面if (pb)又覆盖了pc-next就会把刚接上的pa后半段丢掉。所以要么用if ... else if要么直接像我那样用三元表达式。第三类是不带头结点引发的头指针更新问题。如果你把两条不带头结点的链表拿来合并就必须在每次移动最小节点后判断是否第一个节点如果是则要更新Lc头指针。这个逻辑很容易漏。解决的简单办法是先在函数内部定义一个新的头结点指针head让它始终指向已经接好的最后一个节点最后用Lc head-next返回。这本质上是给自己造了一个临时哑结点。5.2 用打印链表调试法快速定位遇到输出不对很多人的第一反应是盯着代码瞪眼。我推荐的做法是在每个循环关键位置加上打印语句用最笨但最有效的方式看指针到底走到哪了。比如你可以在循环开头加上一句printf(pa%d pb%d\n, pa ? pa-data : -1, pb ? pb-data : -1);再在循环结束后打印整个结果链。这样你能非常直观地看到两个指针是怎么交替消费节点的一旦某一步接错了输出的数字顺序会立刻暴露问题。我自己调试时还会做一个短小用例策略不要用十个节点的数据去测用三四个节点的最小用例。比如{1, 3}和{2, 4}一共四个节点手动模拟一遍指针的变化再对照代码的执行过程很快就能定位到是哪一步的逻辑和预期不一致。5.3 几个必须想清楚的问题除了代码本身还有几个概念问题建议自己口头回答一遍能讲清楚才算真的掌握了。第一个问题合并后原来La和Lb的头结点去哪了答案是其中一个头结点被复用为Lc的头结点另一个被free释放。如果你理解成合并完三条链表都存在只是内容重合了那说明你对指针指向共享内存的理解还不到位。第二个问题Lc和La是什么关系在我们的代码里Lc初始等于La也就是说Lc和La在合并开始时指向同一个头结点。合并完成后原来的La这种说法已经不再成立了因为它的节点已经被重新组织La这个指针在语义上已经被吸收进了Lc。第三个问题如果把papb时改为优先取pb的节点结果还正确吗正确但稳定性变化了。如果题目没有专门要求通常两种写法都算对如果面试官问了相等时你会选哪个你应当能说出稳定性差异。6. 衍生问题与进阶思考6.1 如果不带头结点代码差在哪里我上面给的版本建立在带头结点的基础上。如果题目或者团队已有的链表实现是不带头结点的你需要重写几处关键逻辑。第一种思路是临时头结点法。函数内部 malloc 一个假的头结点让合并逻辑和带头结点版本保持一致最后把真正的头结点地址返回释放假头结点。这算是最省事的改动而且是很多标准库实现的做法。第二种思路是直接处理头指针。先比较La和Lb的第一个节点谁小谁是新的头然后继继续双指针比较。这里最大的坑是一旦你把第一个节点从原链上移走原链的头指针就要更新否则你后续对原链的遍历会重复碰到已经转移走的旧头节点。如果是 C 语言必须用LinkList *二级指针来改传入的头指针变量否则外层感知不到变化。不带头结点的版本代码量会明显增加而且容易出现头指针指向了错误节点这种隐蔽 bug。所以我现在写链表工具类代码时都默认带头结点哪怕题目要用不带头结点的链表我也会先在心里构建一个虚拟头结点来辅助思考。6.2 递归写法很短但不要在生产中用有人喜欢展示递归解法我也写出来供读者参考LinkList MergeRec(LinkList pa, LinkList pb) { if (pa NULL) return pb; if (pb NULL) return pa; if (pa-data pb-data) { pa-next MergeRec(pa-next, pb); return pa; } else { pb-next MergeRec(pa, pb-next); return pb; } }这个写法的优点是逻辑极简和数学归纳法如出一辙先考虑两个基本情况某条链为空就返回另一条然后递归地将较小节点的next指向剩余两条链合并的结果。缺点也明显递归调用栈的深度和链表长度成正比。如果链表有十万个节点递归深度十万层可能会导致栈溢出。而且每次递归都有函数调用的开销。实际项目里几乎不用这种写法但在面试里写出来能展示你对递归的理解前提是你把栈溢出的风险也主动说出来。6.3 从两个链表扩展到 K 个链表两个有序链表合并是基础力扣上有一道经典题叫合并 K 个升序链表。如果直接思路是两两合并先合并第 1、2 条再合并结果和第 3 条总的复杂度是 O(KN)其中 N 是总节点数。更优的做法是使用优先队列把 K 条链表的当前最小节点放进一个最小堆每次取出堆顶接到结果链然后让该节点的下一个节点入堆这样总复杂度是 O(N log K)。理解了本节的双链表合并之后再去看 K 路归并你会发现核心还是那双指针的思路只不过选最小的动作从比较两个值变成了堆的堆顶操作。这就是能力的迁移。数据结构刷题不能只背题解要看到不同题目之间的骨架其实是同一副。最后再说点题外话。这几年我帮人改代码发现很多入门者不是不会写算法而是被编译环境折磨得够呛。比如在 C 语言中传引用是 C 的语法如果你用gcc而不是g编译会直接报错。这时候你有两个选择要么把文件后缀改成.cpp用g编译要么把所有LinkList 改成二级指针。我给初学者的建议是先在一套环境里跑通不要频繁切换等理解了指针本身再去考虑不同写法之间的等价性。等这个合并函数你能闭着眼写出来再试着把free(Lb)去掉看看用valgrind或者 ASAN 工具能报出什么内存错误这比单纯刷题更能锻炼动手能力。
延伸阅读

更多相关文章

2026/9/30 3:01:35

Ubuntu 从裸机到 Docker 容器化实战:安装、调优与避坑指南

简介:这份资源是面向Ubuntu新手与进阶用户的系统学习与实战指南,覆盖从安装配置到开发环境搭建的完整路径。内容按新手入门、进阶优化、实战项目与资源导航分层展开:入门部分讲解ISO镜像制作启动盘、UEFI与传统BIOS分区方案、apt软件源更新与…

2026/9/30 3:01:35

特殊符号引发换行失控?前端文本断行完整修复指南

下午四点收到运营反馈,页面列表里几个标题在移动端排得七零八落。我打开手机一看,标题里的斜杠、井号、连续点号全在捣乱,中文被顶到下一行,地址栏一样的长串字符直接戳出卡片边框。用一句话概括就是:文字一遇到特殊符…

2026/9/30 3:01:35

Unity3D Shader动态屏幕遮罩:跟随目标开洞的实战指南

简介:这份资源面向Unity3D开发者和Shader学习者,聚焦动态屏幕遮罩效果的实现,可用于游戏视野限制、暗黑探索、聚光灯式可视范围等场景。资源以PDF文档形式呈现,压缩包内共1个文件,体积约66KB,内容围绕Shade…

2026/9/30 4:16:38

数据字典从手工到自动化:元数据采集、字段注释与变更治理实战

1. 数据字典到底是什么:先从一个真实的混乱现场说起数据字典这个词,第一次听到的人十有八九会以为它跟《新华字典》沾点亲戚关系,或者以为是把公司所有数据汇总成一个大表格。我在带新人时最常说的一句话是:你先别急着理解定义&am…

2026/9/30 4:16:38

AI工程化实战:从零构建可交付AI系统

1. 这不是“搭积木”,而是亲手锻造AI系统的完整工程链“AI Engineering from Scratch”——看到这个标题,很多人第一反应是:“哦,又一个从零写个神经网络的教程?”但如果你真这么想,就完全误判了它的分量。…

2026/9/30 4:16:38

Python与Java核心差异解析:语法、运行机制与生态选型指南

做了七八年后端,又带了几年新人,最常听到的问题不是“怎么写接口”,而是“老大,我到底该学Python还是Java?”如果你也在刷这两门语言的入门教程、面试题、环境配置,恭喜你,这篇就是为你准备的。…

2026/9/30 4:16:38

闹钟响后如何选择起床?用行为脚本和睡眠惯性破解回笼觉难题

1. 闹钟响后的那个瞬间,你其实正站在十字路口1.1 为什么说这是“一天中最重要的一秒钟”闹钟响后的前五秒,大部分人还分不清自己是睡着了还是醒着。伸手摸到手机,按掉铃声,然后大脑里瞬间弹出两条路:一条是再躺十分钟&…

2026/9/30 4:16:38

C语言手写哈希表创建原理与教学实践

1. 项目概述:从“icoding数据结构——哈希表创建(详细注释)”看教学级哈希实现的本质“icoding数据结构——哈希表创建(详细注释)”这个标题,一眼就能看出它不是工业级系统里的哈希容器,而是面向…

2026/9/30 4:11:38

多线程卡死排查与治理:四招定位死锁、线程池与阻塞点

多线程程序最让人头疼的不是跑不起来,而是跑着跑着就不动了。进程还在,端口还连着,CPU 曲线平得像一条直线,日志停在某个时间点之后再没吐过一个字,重启一下立刻恢复正常,过几个小时又来一遍。这种"假…

2026/9/29 11:07:23

东莞市品牌网站建设报价常见报错与解决

东莞品牌网站建设报价单背后:一份保姆级建站教程避坑实录 网站做好了没人访问,这大概是很多老板最头疼的事。花了大几万做的品牌站,上线后流量惨淡,比路边摊还冷清。别急着骂外包公司,很多“东莞品牌网站建设报价”里藏着不少猫腻,比如用模板站冒充定制…

2026/9/29 21:48:03

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/9/29 7:00:49

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

2026/9/30 0:01:22

MATLAB+Yalmip+CPLEX实战:综合能源系统优化调度全流程解析

做综合能源系统优化调度这活儿,最痛苦的不是建模本身,而是模型写完之后不知道该怎么求解。看论文里轻飘飘一句“采用Yalmip调用CPLEX求解”,自己上手时却往往卡在环境配置、变量声明、约束写法和求解状态判读上,一耗就是两三天。这…

2026/9/30 0:01:22

I3C比I2C快10倍?RK3576实战:速率、DTS配置与混合总线避坑指南

I3C 比 I2C 快 10 倍?这句话在嵌入式群里传了很久,每次都能吵出一堆截图。前段时间我正好在 RK3576 上调板级 I3C 接口,从控制器寄存器一路摸到 Linux DTS 配置,踩了不少坑,也把这笔速度账彻底算明白了。本文就用 RK35…

2026/9/30 0:01:22

字符串转对象:JSON.parse、new Function与URLSearchParams

“字符串转对象”这几个字,我在技术群里见过的问法至少有十几种:有人拿着一串{a:1,b:2}说 JSON.parse 直接报错,有人要从 URL 里抠出参数,还有人只是想把abc变成能挂属性的东西。js 这门语言里,字符串和对象之间的转换…

2026/9/29 3:53:39

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/29 9:46:12

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/29 6:36:14

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑