力扣解题-82. 删除排序链表中的重复元素 II

29 阅读6分钟

力扣解题-82. 删除排序链表中的重复元素 II

给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。

示例 1:

image.png 输入:head = [1,2,3,3,4,4,5]

输出:[1,2,5]

示例 2:

image.png 输入:head = [1,1,1,2,3] 输出:[2,3]

提示:

链表中节点数目在范围 [0, 300] 内

-100 <= Node.val <= 100

题目数据保证链表已经按升序 排列

Related Topics

链表、双指针


第一次解答

解题思路

核心方法:前驱指针+跳过重复段法,通过哑节点(dummy)处理头节点重复的边界问题,利用前驱指针pre定位非重复节点,当发现重复值时,跳过所有连续重复的节点,仅保留无重复的节点,时间复杂度O(n)、空间复杂度O(1),是本题的经典最优解法。

核心逻辑拆解

删除排序链表中所有重复节点的核心是“识别连续重复段并整体跳过”(而非仅删除单个重复节点):

  1. 边界处理:若原链表为空(head == null),直接返回null;
  2. 哑节点初始化:创建dummy哑节点(dummy.next = head),pre指针初始指向dummy(作为当前非重复段的尾节点);
  3. 遍历检测重复
    • 循环条件:pre.next != null && pre.next.next != null(确保有至少两个节点可比较);
    • 检测重复:若pre.next.val == pre.next.next.val(发现重复值):
      • 记录重复值val = pre.next.val
      • 定义temp指针从pre.next开始,向后遍历跳过所有值为val的节点;
      • pre.next指向temp(跳过整个重复段,直接连接到第一个非重复节点);
    • 无重复:pre指针后移一位(pre = pre.next),继续检测下一段;
  4. 返回结果:返回dummy.next(跳过哑节点,得到仅含唯一值的链表)。
具体步骤(以示例1 head=[1,2,3,3,4,4,5]为例)
步骤操作pre指向pre.next指向链表状态说明
1初始化dummy→1→2→3→3→4→4→5dummy1-哑节点避免头节点边界问题
2检测1≠2dummy1-无重复,pre后移
3pre=1,检测2≠312-无重复,pre后移
4pre=2,检测3=323-发现重复,记录val=3
5temp跳过所有3,指向423-跳过重复段3→3
6pre.next=424dummy→1→2→4→4→5连接非重复节点
7检测4=424-发现重复,记录val=4
8temp跳过所有4,指向524-跳过重复段4→4
9pre.next=525dummy→1→2→5连接非重复节点
10循环终止(pre.next.next=null)25-遍历完成
最终链表:1→2→5,与示例结果一致。
性能说明
  • 时间复杂度:O(n)(每个节点仅被访问一次,跳过重复段时的遍历是线性的,无嵌套);
  • 空间复杂度:O(1)(仅使用几个指针变量,无额外存储);
  • 优势:
    1. 一次遍历完成所有操作,跳过重复段而非逐个删除,效率高;
    2. 哑节点完美解决“头节点全重复”的边界问题(如示例2中1→1→1→2→3);
    3. 逻辑清晰,通过“定位重复值→跳过重复段”的思路,精准删除所有重复节点。
    public ListNode deleteDuplicates(ListNode head) {
        if (head == null) return null;
        ListNode dummy=new ListNode(0);
        dummy.next=head;
        ListNode pre=dummy;
        while(pre.next!=null&&pre.next.next!=null){
            if(pre.next.val==pre.next.next.val){
                int val=pre.next.val;
                ListNode temp=pre.next;
                while(temp!=null&&temp.val==val){
                    temp=temp.next;
                }
                pre.next=temp;
            }else {
                pre=pre.next;
            }
        }
        return dummy.next;
    }

示例解答

解题思路

解法1:双指针优化版(更直观的指针命名)

核心方法:哨兵节点+快慢指针,将pre命名为sentinel(哨兵),curr命名为current(当前检测节点),逻辑与原解法一致,但指针命名更贴合语义,代码可读性更高,时间/空间复杂度仍为O(n)/O(1)。

代码实现
public ListNode deleteDuplicates(ListNode head) {
    if (head == null) {
        return null;
    }
    // 哨兵节点(哑节点)
    ListNode sentinel = new ListNode(0);
    sentinel.next = head;
    // 慢指针:指向当前无重复的最后一个节点
    ListNode slow = sentinel;
    
    while (slow.next != null && slow.next.next != null) {
        // 快指针:检测重复的起始节点
        ListNode fast = slow.next;
        // 发现重复值
        if (fast.val == fast.next.val) {
            int duplicateVal = fast.val;
            // 跳过所有重复节点
            while (fast != null && fast.val == duplicateVal) {
                fast = fast.next;
            }
            // 连接到第一个非重复节点
            slow.next = fast;
        } else {
            // 无重复,慢指针后移
            slow = slow.next;
        }
    }
    return sentinel.next;
}
优势说明
  • 指针命名更语义化:slow(慢指针)对应原解法的prefast(快指针)对应原解法的temp,新手更容易理解“慢指针守边界,快指针探重复”的逻辑;
  • 代码结构更清晰:将temp的初始化和遍历整合到fast指针中,减少临时变量的定义;
  • 性能与原解法完全一致,仅代码风格优化。
解法2:递归法(思路拓展)

核心方法:递归缩小问题规模,递归判断当前节点是否与下一个节点重复:

  • 若重复:跳过所有重复节点,递归处理剩余链表;
  • 若无重复:当前节点的next指向递归处理后的链表,返回当前节点; 逻辑优雅但递归深度最多为n(无栈溢出风险,n≤300)。
代码实现
public ListNode deleteDuplicates(ListNode head) {
    // 递归终止:空链表或单节点链表
    if (head == null || head.next == null) {
        return head;
    }
    // 检测当前节点是否重复
    if (head.val == head.next.val) {
        // 跳过所有重复节点
        ListNode curr = head;
        while (curr != null && curr.val == head.val) {
            curr = curr.next;
        }
        // 递归处理剩余链表
        return deleteDuplicates(curr);
    } else {
        // 无重复,递归处理下一个节点
        head.next = deleteDuplicates(head.next);
        return head;
    }
}
适用场景说明
  • 时间复杂度:O(n)(每个节点仅被访问一次);
  • 空间复杂度:O(n)(递归调用栈深度,最坏情况链表无重复,递归深度=n);
  • 优势:代码极简,符合分治思想,无需维护复杂的指针关系;
  • 局限性:递归栈有额外空间开销,执行效率略低于迭代法,且无法处理“头节点重复”的边界问题(需依赖递归终止条件自然跳过)。

总结

  1. 前驱指针跳过重复段法(第一次解答):O(n)时间+O(1)空间,逻辑清晰、边界处理完善,是工程首选的经典迭代解法;
  2. 双指针优化版:指针命名更语义化,代码可读性更高,性能与原解法一致;
  3. 递归法:思路优雅、代码简洁,但有栈空间开销,适合理解递归处理链表的逻辑;
  4. 关键技巧:
    • 核心思想:排序链表的重复节点是连续的,只需“识别重复值→跳过整个重复段”,而非逐个删除;
    • 边界处理:哑节点是解决“头节点全重复”的关键,所有迭代解法均需优先使用;
    • 效率优化:跳过重复段的遍历是线性的,避免嵌套循环,保证O(n)的时间复杂度。