力扣刷题笔记:合并两个有序链表(LeetCode21)—— 双指针 + 虚拟头节点轻松搞定

大家好呀~ 今天来刷链表的经典入门题 ——LeetCode 21 题 “合并两个有序链表”。这题是链表操作的基础,学会它能摸清链表的核心玩法,我们一起一步步拆解,保证学得明明白白~

题目

21. 合并两个有序链表 - 力扣(LeetCode)

题目是说:给你两个非递减顺序排列的链表list1和list2,请你将它们合并成一个同样按非递减顺序排列的新链表。新链表应该由原来两个链表的节点组成(不能新建节点,要复用原节点)。

举个栗子:

  • 输入:list1 = [1,2,4],list2 = [1,3,4] → 输出:[1,1,2,3,4,4]
  • 输入:list1 = [],list2 = [] → 输出:[]
  • 输入:list1 = [],list2 = [0] → 输出:[0]

提示:两个链表的节点数目范围是 0 到 50,节点值范围是 - 100 到 100,且都按非递减顺序排列~

解题思路:双指针 + 虚拟头节点

链表题最容易踩的坑就是 “头节点处理”—— 比如刚开始不知道新链表的头是谁,每次加节点还要判断是不是第一个。这时候 “虚拟头节点” 就是救星!再配合双指针遍历两个原链表,思路直接拉满:

1.虚拟头节点:创建一个临时的 “假头”(比如叫dummy),它的next才是真正的新链表头。这样不管两个原链表是不是空的,都能统一处理加节点的逻辑。

2.双指针遍历:用p1指向list1的头,p2指向list2的头,再用cur指向dummy(负责构建新链表)。

3.比较与链接

  • 如果p1的值 ≤ p2的值,就把p1接到cur的next,然后p1往后移一步;​
  • 否则就把p2接到cur的next,然后p2往后移一步;​
  • 每次接完节点,cur也往后移一步,保持在新链表的末尾。

4.处理剩余节点:当其中一个链表遍历完(p1或p2变成null),把另一个链表剩下的节点直接接到cur的next(因为原链表是有序的,剩下的节点肯定比之前的都大)。

5.返回结果:最后返回dummy.next,就是合并后的新链表头。

代码实现(Java 版)

话不多说,上代码!注释写得超详细,跟着走一遍就懂~

// 先定义链表节点类(力扣里会自带,这里写出来方便理解)
class ListNode {
    int val;
    ListNode next;
    ListNode() {}
    ListNode(int val) { this.val = val; }
    ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}

class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        // 1. 创建虚拟头节点,避免处理头节点为空的麻烦
        ListNode dummy = new ListNode(-1);
        // cur指针:负责构建新链表,初始指向虚拟头节点
        ListNode cur = dummy;
        // p1、p2指针:分别遍历list1和list2
        ListNode p1 = list1;
        ListNode p2 = list2;
        
        // 2. 双指针遍历两个链表,直到其中一个遍历完
        while (p1 != null && p2 != null) {
            if (p1.val <= p2.val) {
                // 把p1接到新链表末尾
                cur.next = p1;
                // p1往后移一步
                p1 = p1.next;
            } else {
                // 把p2接到新链表末尾
                cur.next = p2;
                // p2往后移一步
                p2 = p2.next;
            }
            // cur往后移一步,保持在新链表末尾
            cur = cur.next;
        }
        
        // 3. 处理剩余节点(其中一个链表已经遍历完,剩下的直接接)
        cur.next = (p1 != null) ? p1 : p2;
        
        // 4. 返回真正的新链表头(虚拟头节点的next)
        return dummy.next;
    }
}

代码逐行拆解​

1. 链表节点类​

力扣中会默认提供ListNode类,这里写出来是为了让新手明白节点的结构:每个节点有val(值)和next(指向下一个节点的指针),三个构造方法分别对应 “空节点”“只有值的节点”“有值且有后继的节点”。​

2. 初始化指针

ListNode dummy = new ListNode(-1); // 虚拟头节点,值随便设(不用管)
ListNode cur = dummy; // 构建新链表的指针
ListNode p1 = list1; // 遍历list1的指针
ListNode p2 = list2; // 遍历list2的指针

虚拟头节点的核心作用是 “统一逻辑”—— 比如当两个链表都为空时,dummy.next就是null;当其中一个为空时,直接接另一个链表,不用单独判断 “第一个节点该接谁”。​

3. 双指针遍历与链接

while (p1 != null && p2 != null) {
    if (p1.val <= p2.val) {
        cur.next = p1;
        p1 = p1.next;
    } else {
        cur.next = p2;
        p2 = p2.next;
    }
    cur = cur.next;
}
  • 循环条件:只有当p1和p2都不为空时,才需要比较值(如果有一个为空,就不用比了,直接接剩下的)。​
  • 接节点逻辑:每次选值更小的节点接到cur.next,然后对应的遍历指针(p1或p2)后移,同时cur也后移,保证cur始终在新链表的末尾。​

4. 处理剩余节点

cur.next = (p1 != null) ? p1 : p2;

比如list1遍历完了(p1=null),list2还剩[3,4],就把list2剩下的节点直接接到cur.next—— 因为原链表是有序的,剩下的节点肯定比之前的都大,不用再比较。​

5. 返回结果

return dummy.next;

虚拟头节点的next才是合并后的新链表头,比如例子中dummy.next就是第一个1,后面跟着1,2,3,4,4。​

举个完整例子​

用list1 = [1,2,4],list2 = [1,3,4]走一遍:​

  1. 初始化:dummy(-1),cur=dummy,p1=1(list1 头),p2=1(list2 头)。​
  2. 第一次循环:p1.val=1 ≤ p2.val=1 → cur.next=p1(dummy.next=1),p1=2,cur=1。​
  3. 第二次循环:p1.val=2 > p2.val=1 → cur.next=p2(1.next=1),p2=3,cur=1。​
  4. 第三次循环:p1.val=2 ≤ p2.val=3 → cur.next=p1(1.next=2),p1=4,cur=2。​
  5. 第四次循环:p1.val=4 > p2.val=3 → cur.next=p2(2.next=3),p2=4,cur=3。​
  6. 第五次循环:p1.val=4 ≤ p2.val=4 → cur.next=p1(3.next=4),p1=null,cur=4。​
  7. 循环结束(p1=null):cur.next=p2(4.next=4)。​
  8. 最终dummy.next是1→1→2→3→4→4,返回正确!​

时间和空间复杂度

  • 时间复杂度:O (m + n),其中 m 是 list1 的长度,n 是 list2 的长度。因为每个节点只遍历一次,没有多余操作。​
  • 空间复杂度:O (1)。只用到了几个指针(dummy、cur、p1、p2),没有额外开辟和链表长度相关的空间,属于 “原地合并”(复用原节点)。​

常见坑点提醒​

  1. 忘记移动 cur 指针:如果只接节点但不移动cur,cur会一直停在原地,新链表永远只有一个节点,最后结果肯定错。​
  2. 忽略剩余节点:比如只遍历到其中一个链表为空就结束,没把剩下的节点接上,会导致结果缺失。​
  3. 头节点处理混乱:不用虚拟头节点的话,要单独判断 “list1 为空返回 list2”“list2 为空返回 list1”“两个都不为空选小的当头”,逻辑容易绕错,建议固定用虚拟头节点。​

链表知识点回顾​

刷完这题,咱趁热打铁回顾下链表的核心知识点,以后遇到链表题就不怕啦~​

1. 链表的基本结构​

  • 节点:链表的基本单位,包含 “值(val)” 和 “后继指针(next)”,next 指向 null 表示链表末尾。​
  • 类型:​
  • 单链表:每个节点只有 next 指针(本题就是单链表);​
  • 双链表:每个节点多一个 prev 指针,可向前遍历;​
  • 循环链表:末尾节点的 next 指向头节点,可循环遍历。​
  • 特点:​
  • 优点:不需要连续内存,插入 / 删除节点(不考虑查找)时间复杂度 O (1);​
  • 缺点:不能随机访问(要找第 k 个节点必须从头遍历),时间复杂度 O (n)。​

2. 链表的核心操作​

(1)遍历链表​

遍历是链表操作的基础,比如找链表长度、打印链表值:

// 遍历链表并打印值
public void traverse(ListNode head) {
    ListNode cur = head;
    while (cur != null) {
        System.out.print(cur.val + " ");
        cur = cur.next; // 指针后移,不能漏!
    }
}

(2)插入节点​

分 “头插”“中间插”“尾插”,核心是 “先连后断”(避免节点丢失):​

  • 头插(在 head 前插新节点):
    ListNode newNode = new ListNode(0);
    newNode.next = head; // 新节点先连原头
    head = newNode; // 头指针指向新节点
  • 尾插(在末尾加节点,需先找到尾节点):
    ListNode newNode = new ListNode(5);
    ListNode cur = head;
    while (cur.next != null) { // 找尾节点(cur.next为null时cur是尾)
        cur = cur.next;
    }
    cur.next = newNode; // 尾节点的next连新节点

    (3)删除节点​

    同样分 “删头”“删中间”“删尾”,核心是 “找到前驱节点”:​

  • 删头(删除第一个节点):

if (head != null) {
    head = head.next; // 头指针直接后移,原头节点会被GC回收
}
  • 删指定值的节点(比如删值为 3 的节点):
    ListNode dummy = new ListNode(-1);
    dummy.next = head;
    ListNode cur = dummy;
    while (cur.next != null) {
        if (cur.next.val == 3) {
            cur.next = cur.next.next; // 跳过要删的节点(直接连下下个)
            break;
        }
        cur = cur.next;
    }
    head = dummy.next;

    3. 链表题的常用技巧​

  • 虚拟头节点:处理头节点为空或需要头插 / 删头的场景(本题核心技巧),统一代码逻辑。​
  • 双指针:​
  • 快慢指针:找链表中点、判断环、找环入口;​
  • 前后指针:遍历 + 修改(比如反转链表);​
  • 本题的 “双指针遍历两个链表” 也属于此类。​
  • 递归:某些链表题用递归更简洁(比如本题也能递归实现,不过迭代法更直观)。​

总结​

合并两个有序链表是链表的入门题,核心是 “虚拟头节点 + 双指针”—— 虚拟头节点解决头节点处理麻烦的问题,双指针实现有序合并。掌握这两个技巧,以后遇到链表的 “合并”“拼接” 类问题都能举一反三。​

最后回顾的链表知识点一定要记牢,尤其是遍历、插入、删除的基本操作,这些是所有链表题的基础。赶紧去力扣试试这题,有问题评论区问我呀,下次见~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值