82. 删除排序链表中的重复元素 II

82. 删除排序链表中的重复元素 II

✨核心逻辑

由于给定的链表是已排序的,所以重复的元素必然在链表中连续相邻。题目要求删除所有重复数字的节点,只留下不同的数字。这里提供两种解题思路:

  1. 双指针法(快慢指针跳跃):利用快慢指针 slowfast 遍历链表。如果 slowfast 指向的节点值不相等,则说明当前 slow 是可以保留的;如果值相等,则利用内部循环将 slowfast 共同向后移动,跳过所有连续的相同节点,达到删除整个重复段的目的。

🔥代码实现(含详细变量注释)

public class deleteDuplicates_82 {
    
    // ================= 方法 1:双指针法 =================
    public ListNode deleteDuplicates(ListNode head) {
        // 边界情况判断:如果链表为空,直接返回
        if (head == null) {
            return head;
        }
        
        // slow:慢指针,用于指向当前待比较的节点
        ListNode slow = head;
        // fast:快指针,用于探测 slow 后面的节点
        ListNode fast = head.next;
        
        // result:虚拟头节点,用于存储最终保留下来的不重复节点结果链
        ListNode result = new ListNode(0);
        // copy:结果链表的尾指针,用于串联保留的节点
        ListNode copy = result;
        
        // 当慢指针和快指针都不为空时,进行遍历对比
        while (slow != null && fast != null) {
            // 如果当前两个指针指向的节点值不相等,说明 slow 是安全的不重复节点,可以保留
            if (slow.val != fast.val) {
                copy.next = slow;         // 将 slow 接到结果链表中
                copy = copy.next;         // 更新结果链表的尾指针
                slow = slow.next;         // 慢指针前进一步
                fast = fast.next;         // 快指针前进一步
            } else {
                // 如果值相等,说明进入了重复段
                // 内部循环:跳过所有与 slow 当前值相同的节点
                while (fast != null && slow.val == fast.val) {
                    slow = slow.next;
                    fast = fast.next;
                }
                // 因为 slow 目前指向的是重复段的最后一个节点,需要再向前一步,跳过整个重复段
                slow = slow.next;
                // 如果快指针还未空,同样再向前一步,准备进行下一步的扫描
                if (fast != null) {
                    fast = fast.next;
                }
            }
        }
        
        // 当循环结束时,如果是尾部出现重复节点被跳过,最终只剩一个孤立重复节点,这一步需要做最后的安全拼接
        copy.next = slow;
        
        // 返回虚拟头节点的下一个节点,即去重后的真实头节点
        return result.next;
    }
  1. 单指针(前驱指针)连接法:借助一个虚拟头节点(防止头节点被删找不到头),利用 cur 指针查找当前节点和下个节点。如果发现 cur.next.val == cur.next.next.val,则记录下这个重复的值,并用内部 while 循环,直接将 cur.next 跳跃到最后一个重复节点之后,达到一次性删除所有相同值节点的目的。
    // ================= 方法 2:单指针(前驱指针)法 =================
    public ListNode deleteDuplicates1(ListNode head) {
        // 基础边界判断
        if (head == null || head.next == null) {
            return head;
        }
        
        // 1. 创建哑节点(虚拟头节点),指向 head。
        // 作用:防止原链表头节点自身就是重复元素被删掉,导致无法返回新的头节点
        ListNode dummy = new ListNode(0, head);
        // cur:用于遍历的前驱指针,初始指向哑节点
        ListNode cur = dummy; 
        
        // 检查当前节点之后是否至少还有两个节点
        while (cur.next != null && cur.next.next != null) {
            // 如果当前节点的下个节点,和下下个节点的值相同,说明出现了重复段
            if (cur.next.val == cur.next.next.val) {
                // 记录当前这个重复的值
                int val = cur.next.val;
                // 内部循环:只要下个节点不为空,且它和记录的值相同
                // 就不断将 cur.next 指向 cur.next.next,即跳过(删除)这个重复节点
                while (cur.next != null && cur.next.val == val) {
                    cur.next = cur.next.next;
                }
            } else {
                // 如果不重复,前驱指针正常向后移动一位
                cur = cur.next;
            }
        }
        // 返回虚拟头节点的下一个节点,即处理后的新头节点
        return dummy.next;
    }
}
  • ⏱️复杂度分析
    • 时间复杂度:O(N),其中 N 是链表的长度。无论是双指针法还是单指针法,链表中的每个节点最多只会被遍历和访问 1~2 次,整体时间复杂度呈线性。

    • 空间复杂度:O(1),两种方法都只使用了少量的指针变量(如 slow、fast、dummy、cur、copy 等),没有创建新的链表节点空间,为原地操作。

🔍总结一下:

其中,思路一是自己写的,只不过需要额外注意 fast 为 null 的情况,需要额外写判空条件。思路二是官方写法,只用到了一个指针,使用 next 以及 next.next 去判断后两个节点