合并两个有序链表的核心是双指针归并:用哑节点简化头处理,比较两链表当前节点值,取小者接入结果链并前移对应指针,循环结束后将非空链剩余部分整体接上,不拷贝节点、仅重连指针,注意空指针判断与边界处理。

合并两个有序链表的核心逻辑是双指针归并
直接用 std::list 或手写单链表都没关系,关键不是“怎么建链表”,而是“怎么在不破坏有序性前提下把两段已排好序的节点串成一条”。你不需要先遍历一遍转成数组再排序——那样就失去“有序”这个前提的价值了。
常见错误是:一边遍历一边 new 节点复制值,浪费内存;或者漏处理某条链表剩余部分,导致结果截断;还有人试图递归但没设好终止条件,栈溢出。
- 必须用两个指针分别指向两链表当前待比较节点(比如
p1和p2) - 用一个哑节点(
dummy)简化头结点处理,避免单独判断谁做新头 - 每次取较小值节点“接入”结果链,对应指针前移,另一指针不动
- 循环结束后,必有一条链非空,直接接在结果末尾即可
手写单链表时注意节点所有权和内存管理
如果你用的是自定义结构体(比如 struct ListNode { int val; ListNode* next; };),函数签名通常是 ListNode* mergeTwoLists(ListNode* list1, ListNode* list2)。这里没有拷贝节点,只是重连 next 指针——所以输入链表会被“拆解”,原链表结构不再完整。
这意味着:不能在合并后还假设 list1 或 list2 保持原样;如果业务需要保留原链,就得深拷贝节点,但那就不是“合并有序链表”的本意了。
- 不要在循环里对
p1或p2做delete,你只是重定向指针 - 如果输入可能为空(
nullptr),开头就要判空,否则p1->val会崩溃 - 返回的是
dummy->next,别忘了释放dummy(或用栈上对象避免 new)
用 std::list 时别误用 merge() 成员函数
std::list 确实有 merge() 成员函数,但它要求**调用方链表本身有序**,且会把参数链表的所有节点搬过来、清空参数链表。很多人没注意到这点,传入两个独立的 std::list 后发现一个变空了,另一个变长了,还以为出 bug。
- 正确用法:
list1.merge(list2);—— 此时list2变为空,list1包含全部合并后元素 - 前提是
list1和list2都已升序(或都降序),且比较器一致 - 它底层就是归并,时间复杂度 O(m+n),但会修改原容器,不适合只读场景
- 如果不想改原链,得自己遍历 +
insert(),但性能差,不推荐
LeetCode 21 题的边界 case 容易踩坑
测试用例常包含全空、一空一非空、单节点、大量重复值等情况。比如 [1,1,2] 和 [1,3,4] 合并后是 [1,1,1,2,3,4],不是去重。
最容易忽略的是:当某链走完后,另一链剩下的所有节点必须整体接上,而不是只接一个。
- 别写成
if (p1) res->next = p1;—— 这只接了一个节点,后面丢了 - 要写成
res->next = p1 ? p1 : p2;或分两 branch 显式接整个子链 - 如果用递归写法,终止条件必须是
if (!l1) return l2; if (!l2) return l1;,缺一不可 - 整数溢出一般不在这题出现,但若 val 是
short或有符号类型,比较时别隐式提升出问题
归并本身很简单,难的是写干净、不漏分支、不崩空指针、不误改原数据。链表操作一旦指针乱了,调试比数组麻烦得多。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











