【算法--链表】删除排序链表中的重复元素 II--通俗讲解

47 阅读4分钟

一、题目是啥?一句话说清

给你一个已经排序的链表,删除所有重复数字的节点(包括本身),只留下不同的数字,并返回处理后的链表。

示例:

  • 输入:1 → 1 → 2 → 3 → 3
  • 输出:2(因为所有1和3都被删除,只留下2)

二、解题核心

使用虚拟头节点简化操作,遍历链表,对于每个节点,检查其后是否有重复节点,如果有则跳过整个重复序列。 这就像处理一排已经排序的队伍,如果发现连续多个人身高相同,就让所有这些人都离开队伍。

三、关键在哪里?(3个核心点)

想理解并解决这道题,必须抓住以下三个关键点:

1. 虚拟头节点(Dummy Node)的使用

  • 是什么:在原始链表前添加一个不存储实际数据的节点。
  • 为什么重要:因为头节点可能被删除(如示例中头节点1被删除),使用虚拟头节点可以避免单独处理头节点变化的情况。

2. 跳过整个重复序列

  • 是什么:当发现重复时,不是只跳过下一个节点,而是找到重复序列的结束位置,然后一次性跳过所有重复节点。
  • 为什么重要:因为题目要求删除所有重复数字的节点,而不是保留一个。

3. 指针操作的安全性

  • 是什么:在遍历链表时,需要确保指针不为空,避免空指针异常。
  • 为什么重要:因为可能操作到链表的末尾,需要谨慎检查指针的next属性。

四、看图理解流程(通俗理解版本)

让我们用链表 1 → 1 → 2 → 3 → 3 的例子来可视化过程:

  1. 初始化

    • 创建虚拟头节点 dummy,其 next 指向头节点 1。
    • 设置当前指针 curr 指向 dummy
    • 初始状态:dummy → 1 → 1 → 2 → 3 → 3
  2. 第一轮检查

    • curr 指向 dummy,检查 curr.next (1) 和 curr.next.next (1) 的值是否相等。
    • 相等,说明有重复,需要删除所有值为1的节点。
    • 找到第一个值不为1的节点(节点2)。
    • curr.next 指向节点2(跳过所有1)。
    • 链表状态:dummy → 2 → 3 → 3
    • curr 仍然指向 dummy(因为可能新的开头还有重复)。
  3. 第二轮检查

    • curr 指向 dummy,检查 curr.next (2) 和 curr.next.next (3) 的值是否相等。
    • 不相等,说明没有重复,移动 currcurr.next(指向2)。
    • 链表状态:dummy → 2 → 3 → 3
  4. 第三轮检查

    • curr 指向2,检查 curr.next (3) 和 curr.next.next (3) 的值是否相等。
    • 相等,说明有重复,需要删除所有值为3的节点。
    • 找到第一个值不为3的节点(null)。
    • curr.next 指向 null(跳过所有3)。
    • 链表状态:dummy → 2
  5. 结束

    • 返回 dummy.next,即节点2。

五、C++ 代码实现(附详细注释)

#include <iostream>
using namespace std;

// 链表节点定义
struct ListNode {
    int val;
    ListNode *next;
    ListNode() : val(0), next(nullptr) {}
    ListNode(int x) : val(x), next(nullptr) {}
    ListNode(int x, ListNode *next) : val(x), next(next) {}
};

class Solution {
public:
    ListNode* deleteDuplicates(ListNode* head) {
        // 处理空链表或单节点链表
        if (head == nullptr || head->next == nullptr) {
            return head;
        }
        
        // 创建虚拟头节点,简化操作
        ListNode* dummy = new ListNode(0);
        dummy->next = head;
        ListNode* curr = dummy; // 当前指针,指向当前处理节点的前一个节点
        
        while (curr->next != nullptr && curr->next->next != nullptr) {
            // 检查当前节点的下一个节点和下下个节点是否值相等
            if (curr->next->val == curr->next->next->val) {
                int duplicateVal = curr->next->val; // 记录重复值
                // 跳过所有值为duplicateVal的节点
                while (curr->next != nullptr && curr->next->val == duplicateVal) {
                    ListNode* temp = curr->next;
                    curr->next = curr->next->next;
                    delete temp; // 释放内存(实际面试中可能不需要)
                }
            } else {
                // 没有重复,移动到下一个节点
                curr = curr->next;
            }
        }
        
        ListNode* newHead = dummy->next;
        delete dummy; // 释放虚拟头节点内存
        return newHead;
    }
};

// 辅助函数:打印链表
void printList(ListNode* head) {
    while (head != nullptr) {
        cout << head->val << " ";
        head = head->next;
    }
    cout << endl;
}

// 测试代码
int main() {
    // 创建示例链表:1->1->2->3->3
    ListNode* head = new ListNode(1, new ListNode(1, new ListNode(2, new ListNode(3, new ListNode(3))));
    
    Solution solution;
    ListNode* result = solution.deleteDuplicates(head);
    
    printList(result); // 输出:2
    
    // 释放内存(实际面试中可能不需要完整释放)
    return 0;
}

六、注意事项

  • 内存管理:在C++中,如果删除了节点,最好释放其内存,但面试中通常更关注算法逻辑,可以不写delete。
  • 指针判空:在访问curr->nextcurr->next->next之前,确保currcurr->next不为nullptr,避免空指针异常。
  • 循环条件:外部循环检查curr->nextcurr->next->next,内部循环检查curr->next
  • 特殊情况:链表为空或只有一个节点时,直接返回,不需要处理。

七、总结

理解此题的关键在于:

  • 使用虚拟头节点:处理头节点可能被删除的情况。
  • 跳过整个重复序列:当发现重复时,找到重复序列的结束位置,一次性跳过所有重复节点。
  • 谨慎的指针操作:确保在操作指针时不会出现空指针异常。

掌握这三点,你就能高效解决删除排序链表中的重复元素 II 问题。这道题是链表操作的进阶题目,考察了对链表遍历和指针操作的深入理解。多练习几次,注意细节,就能熟练运用。