【算法】【链表】203.移除链表元素--通俗讲解

58 阅读5分钟

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

给定一个链表和一个整数值,删除链表中所有值等于该整数的节点,并返回新的头节点。

示例:

  • 输入:1 → 2 → 6 → 3 → 4 → 5 → 6, val = 6
  • 输出:1 → 2 → 3 → 4 → 5

二、解题核心

使用虚拟头节点简化操作,遍历链表,当遇到值等于目标值的节点时,跳过该节点(即修改前一个节点的next指针)。

这就像在一条队伍中,我们要移除所有穿红色衣服的人,我们只需要让每个人记住他后面的人是谁,当遇到穿红衣服的人时,就直接记住他后面的人,跳过这个穿红衣服的人。

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

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

1. 虚拟头节点的使用

  • 是什么:创建一个虚拟头节点,其next指向原链表的头节点,这样可以简化删除头节点的操作。
  • 为什么重要:因为头节点也可能需要被删除,使用虚拟头节点可以避免处理特殊的头部删除情况,使代码更统一和简洁。

2. 指针的遍历和跳过

  • 是什么:遍历链表,当当前节点的值等于目标值时,修改前一个节点的next指针,跳过当前节点。
  • 为什么重要:这是删除节点的核心操作,需要正确更新指针,确保链表不断裂,同时跳过所有值等于目标值的节点。

3. 内存管理(在C++中)

  • 是什么:在删除节点时,需要释放被删除节点的内存,避免内存泄漏。
  • 为什么重要:良好的内存管理是C++编程的基本要求,避免内存泄漏可以提高程序的稳定性和效率。

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

假设链表为:1 → 2 → 6 → 3 → 4 → 5 → 6,要删除值为6的节点。

  1. 初始化

    • 创建虚拟头节点dummy,dummy.next指向1。
    • 设置prev指针指向dummy,curr指针指向1。
  2. 遍历和删除

    • 检查curr的值(1)≠6,prev和curr都前进:prev指向1,curr指向2。
    • 检查curr的值(2)≠6,prev和curr都前进:prev指向2,curr指向6。
    • 检查curr的值(6)=6,执行删除:
      • 将prev的next指向curr的next(即2的next指向3)。
      • 删除curr节点(释放内存)。
      • curr指向3(prev的next)。
    • 检查curr的值(3)≠6,prev和curr都前进:prev指向3,curr指向4。
    • 检查curr的值(4)≠6,prev和curr都前进:prev指向4,curr指向5。
    • 检查curr的值(5)≠6,prev和curr都前进:prev指向5,curr指向6。
    • 检查curr的值(6)=6,执行删除:
      • 将prev的next指向curr的next(即5的next指向null)。
      • 删除curr节点(释放内存)。
      • curr指向null。
    • 遍历结束。
  3. 返回结果:返回dummy.next,即1 → 2 → 3 → 4 → 5。

五、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* removeElements(ListNode* head, int val) {
        // 创建虚拟头节点,简化操作
        ListNode* dummy = new ListNode(0);
        dummy->next = head;
        
        ListNode* prev = dummy; // 前驱指针
        ListNode* curr = head;  // 当前指针
        
        while (curr != nullptr) {
            if (curr->val == val) {
                // 找到需要删除的节点
                prev->next = curr->next; // 跳过当前节点
                delete curr;             // 释放内存
                curr = prev->next;       // 更新当前指针
            } else {
                // 不需要删除,移动指针
                prev = curr;
                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->2->6->3->4->5->6
    ListNode* head = new ListNode(1);
    head->next = new ListNode(2);
    head->next->next = new ListNode(6);
    head->next->next->next = new ListNode(3);
    head->next->next->next->next = new ListNode(4);
    head->next->next->next->next->next = new ListNode(5);
    head->next->next->next->next->next->next = new ListNode(6);
    
    Solution solution;
    ListNode* result = solution.removeElements(head, 6);
    
    printList(result); // 输出:1 2 3 4 5
    
    // 释放内存(实际应用中需要更完整的释放)
    while (result != nullptr) {
        ListNode* temp = result;
        result = result->next;
        delete temp;
    }
    
    return 0;
}

六、时间空间复杂度

  • 时间复杂度:O(n),其中n是链表长度。需要遍历整个链表一次。
  • 空间复杂度:O(1),只使用了常数额外空间(几个指针),不包括虚拟头节点(但虚拟头节点也是常数空间)。

七、注意事项

  • 虚拟头节点的使用:使用虚拟头节点可以简化删除头节点的操作,避免特殊处理。
  • 内存管理:在C++中,删除节点时需要释放内存,避免内存泄漏。但要注意,在删除节点后,需要更新指针,避免访问已释放的内存。
  • 指针更新:在删除节点时,需要正确更新前驱指针的next指针,确保链表不断裂。
  • 边界情况:处理空链表的情况,以及链表所有节点都需要删除的情况。
  • 代码健壮性:确保在删除节点后,程序能正确处理后续节点,不会出现空指针异常。

** 算法通俗讲解推荐阅读**
【算法--链表】83.删除排序链表中的重复元素--通俗讲解
【算法--链表】删除排序链表中的重复元素 II--通俗讲解
【算法--链表】86.分割链表--通俗讲解
【算法】92.翻转链表Ⅱ--通俗讲解
【算法--链表】109.有序链表转换二叉搜索树--通俗讲解
【算法--链表】114.二叉树展开为链表--通俗讲解
【算法--链表】116.填充每个节点的下一个右侧节点指针--通俗讲解
【算法--链表】117.填充每个节点的下一个右侧节点指针Ⅱ--通俗讲解
【算法--链表】138.随机链表的复制--通俗讲解
【算法】143.重排链表--通俗讲解
【算法--链表】146.LRU缓存--通俗讲解
【算法--链表】147.对链表进行插入排序--通俗讲解
【算法】【链表】148.排序链表--通俗讲解
【算法】【链表】160.相交链表--通俗讲解