LeetCode 21:合并两个有序链表(指针思维真正搞懂版)

47 阅读3分钟

一、题目要求

给你两个 升序排列 的单链表 list1list2
请你将它们合并成一个新的 升序链表,并返回合并后的链表头节点。

示例:

list1: 1 -> 2 -> 4
list2: 1 -> 3 -> 4

输出: 1 -> 1 -> 2 -> 3 -> 4 -> 4

注意几个关键词:

  • 两个链表都是升序
  • 合并后仍然要保持升序
  • 不能破坏链表结构(只调整指针)

二、常见误区

很多人一上来会想:

  • 把所有值取出来,放进数组排序
  • 或者新建一堆节点再拼

这些方法虽然能做出来,但完全绕开了链表这道题真正想考的点

指针如何在两个链表之间移动和衔接


三、核心思路:虚拟头结点 + 双指针

1. 为什么要用“虚拟头结点”

ListNode preHead = new ListNode(-1);

作用只有一个,但非常关键:

  • 避免单独处理“第一个节点”
  • 让所有节点的拼接逻辑完全一致

你只需要关心:

当前节点该接谁,而不用关心“是不是头节点”


2. 两个指针同时往前走

  • list1:指向链表 1 当前节点
  • list2:指向链表 2 当前节点
  • preNext:始终指向合并后链表的最后一个节点

四、完整代码

class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        // 虚拟头结点
        ListNode preHead = new ListNode(-1);
        ListNode preNext = preHead;

        // 同时遍历两个链表
        while (list1 != null && list2 != null) {
            if (list1.val <= list2.val) {
                preNext.next = list1;
                list1 = list1.next;
            } else {
                preNext.next = list2;
                list2 = list2.next;
            }
            preNext = preNext.next;
        }

        // 拼接剩余部分
        preNext.next = list1 == null ? list2 : list1;

        return preHead.next;
    }
}

五、逐行拆解:指针是怎么动的

1. 初始化阶段

ListNode preHead = new ListNode(-1);
ListNode preNext = preHead;

当前结构是:

preHead(-1) -> null
preNext 指向 preHead

此时还没真正开始合并。


2. 主循环条件

while (list1 != null && list2 != null)

含义是:

  • 只要两个链表都有节点
  • 就可以继续比较、继续合并

3. 比较当前节点值

if (list1.val <= list2.val) {
    preNext.next = list1;
    list1 = list1.next;
} else {
    preNext.next = list2;
    list2 = list2.next;
}

这一步在做三件事:

  1. 选出较小的节点
  2. 接到合并链表后面
  3. 被选中的链表指针向前移动

4. 移动合并链表指针

preNext = preNext.next;

这一句非常重要,它保证了:

下一次再接节点时,一定是接在“当前链表的尾部”


5. 拼接剩余链表

preNext.next = list1 == null ? list2 : list1;

为什么可以直接接?

因为:

  • 如果 list1 先空了,list2 剩下的一定是有序的
  • 反过来也一样

这里不需要再比较,直接整体挂上即可。


6. 为什么返回 preHead.next

return preHead.next;

原因很简单:

  • preHead 是人为加的虚拟节点
  • 真正的合并结果从它的下一个节点开始

六、用一个过程图理解

假设:

list1: 1 -> 3 -> 5
list2: 2 -> 4 -> 6

合并过程是:

-1 -> 1 -> 2 -> 3 -> 4 -> 5 -> 6

preNext 每次都只做一件事:

把当前最小的节点接过来,然后自己往后挪一格


七、复杂度分析

  • 时间复杂度:O(n + m)

    • 每个节点只访问一次
  • 空间复杂度:O(1)

    • 只用了常数级指针