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

41 阅读4分钟

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

给你一个已经排序的链表,删除所有重复的元素,使得每个元素只出现一次,并返回处理后的链表。

示例:

  • 输入:1 → 1 → 2 → 3 → 3
  • 输出:1 → 2 → 3

二、解题核心

利用链表已排序的特性,遍历链表,比较当前节点与下一个节点的值,如果相同就跳过下一个节点,否则移动到下一个节点。 这就像处理一排已经按身高排序的队伍,如果发现相邻两个人身高相同,就让后一个人离开队伍。

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

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

1. 利用已排序的特性

  • 是什么:因为链表已经排序,所有重复元素一定是相邻的。
  • 为什么重要:这样只需要一次遍历就能删除所有重复元素,不需要使用额外数据结构(如哈希表)来记录已出现的元素。

2. 指针操作

  • 是什么:使用一个指针遍历链表,比较当前节点与下一个节点的值。
  • 为什么重要:通过调整指针的指向来"跳过"重复节点,而不是实际删除节点(在C++中需要注意内存管理,但算法题通常更关注逻辑)。

3. 边界条件处理

  • 是什么:处理空链表或单节点链表的情况。
  • 为什么重要:这些特殊情况不需要任何操作,直接返回即可,避免程序出错。

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

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

  1. 初始化

    • 设置当前指针 curr 指向头节点 1。
    • 链表状态:1 → 1 → 2 → 3 → 3
  2. 第一轮比较

    • 比较 curr.val (1) 和 curr.next.val (1),两者相等。
    • 跳过下一个节点:让 curr.next 指向 curr.next.next(即第二个1指向2)。
    • 链表状态:1 → 2 → 3 → 3
    • curr 仍然指向第一个1(因为可能还有重复)。
  3. 第二轮比较

    • 再次比较 curr.val (1) 和 curr.next.val (2),两者不相等。
    • 移动 curr 到下一个节点(指向2)。
    • 链表状态:1 → 2 → 3 → 3
  4. 第三轮比较

    • 比较 curr.val (2) 和 curr.next.val (3),两者不相等。
    • 移动 curr 到下一个节点(指向3)。
    • 链表状态:1 → 2 → 3 → 3
  5. 第四轮比较

    • 比较 curr.val (3) 和 curr.next.val (3),两者相等。
    • 跳过下一个节点:让 curr.next 指向 curr.next.next(即null)。
    • 链表状态:1 → 2 → 3
    • curr 仍然指向3。
  6. 结束

    • curr.next 为null,循环结束。
    • 返回头节点1。

五、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* curr = head; // 当前指针
        
        // 遍历链表
        while (curr != nullptr && curr->next != nullptr) {
            if (curr->val == curr->next->val) {
                // 发现重复,跳过下一个节点
                ListNode* duplicate = curr->next;
                curr->next = curr->next->next;
                delete duplicate; // 释放内存(实际面试中可能不需要)
            } else {
                // 没有重复,移动到下一个节点
                curr = curr->next;
            }
        }
        
        return head;
    }
};

// 辅助函数:打印链表
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); // 输出:1 2 3
    
    // 释放内存(实际面试中可能不需要完整释放)
    return 0;
}

六、注意事项

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

七、总结

理解此题的关键在于:

  • 利用已排序特性:重复元素一定相邻,只需一次遍历。
  • 指针操作:通过调整指针的指向来跳过重复节点。
  • 边界处理:正确处理空链表和单节点链表的情况。

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