Leetcode 98 从链表中移除在数组中存在的节点

1 题目

3217. 从链表中移除在数组中存在的节点

给你一个整数数组 nums 和一个链表的头节点 head。从链表中移除 所有存在于 nums 中的节点后,返回修改后的链表的头节点。

示例 1:

输入: nums = 1,2,3, head = 1,2,3,4,5

输出: 4,5

解释:

移除数值为 1, 2 和 3 的节点。

示例 2:

输入: nums = 1, head = 1,2,1,2,1,2

输出: 2,2,2

解释:

移除数值为 1 的节点。

示例 3:

输入: nums = 5, head = 1,2,3,4

输出: 1,2,3,4

解释:

链表中不存在值为 5 的节点。

提示:

  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 105
  • nums 中的所有元素都是唯一的。
  • 链表中的节点数在 [1, 105] 的范围内。
  • 1 <= Node.val <= 105
  • 输入保证链表中至少有一个值没有在 nums 中出现过。

2 代码实现

cpp 复制代码
/**
 * Definition for singly-linked list.
 * 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* modifiedList(vector<int>& nums, ListNode* head) {
        unordered_set <int> numbers(nums.begin(),nums.end());

        if (head == nullptr){
            return head ;
        }
        ListNode* dummy = new ListNode(0);
        dummy -> next = head ;
        ListNode* cur = dummy;
        while(cur -> next != nullptr){
            if (numbers.count(cur -> next -> val)){
                cur -> next = cur -> next -> next;
            }else{
            cur = cur -> next;
            }
        }

        head = dummy -> next ;
        delete dummy;
        return head ;
    }
};

思路

这个题目和之前的,上一篇博客的里的Leetcode 97 移除链表元素-CSDN博客 区别在于现在所要检查的val不是单个输入的值,而是数组。

我想到统计出现次数常用的做法就是用set 或者 map ,这里也许把数组的元素存进set里面,然后count调用函数看看当前遍历元素的val是不是数组的,如果是,删除。

删除的逻辑我采用迭代的做法。

自己一开始提交的代码

有点错误

cpp 复制代码
/**
 * Definition for singly-linked list.
 * 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* modifiedList(vector<int>& nums, ListNode* head) {
        unordered_set <int> numbers;
        for(int i = 0 ; i < nums.size() ; i++){
            numbers.insert(nums[i]);
        }

        if (head == nullptr){
            return head ;
        }
        ListNode* dummy = new ListNode(0);
        dummy -> next = head ;
        ListNode* cur = dummy;
        while(cur -> next != nullptr){
            if (numbers.count(cur -> next -> val)){
                ListNode* temp = cur -> next ;
                cur -> next = cur -> next -> next ;
                delete temp ;
            }
            cur = cur -> next;
        }

        head = dummy -> next ;
        delete dummy;
        return head ;
    }
};

错误分析

核心问题出在 while 循环内的指针移动逻辑:

cpp 复制代码
while(cur -> next != nullptr){
    if (numbers.count(cur -> next -> val)){
        // 删除节点的逻辑是对的
        ListNode* temp = cur -> next ;
        cur -> next = cur -> next -> next ;
        delete temp ;
    }
    // 错误点:无论是否删除节点,都移动 cur 指针
    cur = cur -> next;
}
  • 问题本质 :当删除了 cur->next 节点后,cur 本身不应该移动(因为新的 cur->next 还没检查),但你的代码无条件执行 cur = cur->next,会跳过部分节点的检查,甚至可能导致空指针访问。
  • 举例 :如果连续两个节点都需要删除,第一个节点被删除后,cur 直接移动到下一个节点,第二个需要删除的节点会被漏掉;极端情况下,删除最后一个节点后 cur->nextnullptr,再执行 cur = cur->next 会导致后续访问 cur->next 时报错。

另外,在leetcode平台上我又出现了执行出错。

错误根源分析

  1. 内存释放的权责问题
    • 你在代码中用 delete temp 释放了被删除的节点,但 OJ 测试框架本身会负责管理链表的内存(比如测试结束后自动释放整个链表)。
    • 你提前释放了节点,测试框架后续访问这些已释放的节点时,就会触发 heap-use-after-free 错误。
  2. 本质 :OJ 环境中,不需要手动释放链表节点的内存 (这是测试框架的职责),你的 delete 操作反而破坏了内存管理的一致性。

通过的代码

cpp 复制代码
/**
 * Definition for singly-linked list.
 * 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* modifiedList(vector<int>& nums, ListNode* head) {
        unordered_set <int> numbers;
        for(int i = 0 ; i < nums.size() ; i++){
            numbers.insert(nums[i]);
        }

        if (head == nullptr){
            return head ;
        }
        ListNode* dummy = new ListNode(0);
        dummy -> next = head ;
        ListNode* cur = dummy;
        while(cur -> next != nullptr){
            if (numbers.count(cur -> next -> val)){
                cur -> next = cur -> next -> next;
            }else{
            cur = cur -> next;
            }
        }

        head = dummy -> next ;
        delete dummy;
        return head ;
    }
};

其实一开始存入set的代码可以更简洁一点。

直接用一行

cpp 复制代码
 unordered_set<int> numbers(nums.begin(), nums.end());

3 小结

这题除了处理val数组没有别的难点,我也想了一下递归,实质就是先写个private函数,传参是val,head,就又回到了上一题,完完全全的写法,只是数据判定不同。但我觉得再依靠一个函数有点冗余了。

这道题让我更深理解了迭代中的条件判断,循环控制。

比如我犯了的错,只有当不需要删除操作的时候,cur指针才往下走,否则逻辑很混乱,下一个指针都要被删掉了怎么走一个next呢?

整体很简单。

-------

核心总结

  1. 解题核心思路 :将数组存入unordered_set实现 O (1) 时间复杂度的查找,结合虚拟头节点处理链表删除,迭代遍历链表时仅在不删除节点时移动指针,保证每个节点被检查。
  2. 关键错误与修复
    • 指针逻辑错误:原代码无条件移动cur指针,导致漏检节点 / 空指针,修复后仅在else分支(不删除节点)移动指针;
    • 内存管理错误:手动delete链表节点触发heap-use-after-free,修复后仅做逻辑删除(跳过节点),仅释放自建的dummy节点。
  3. 优化与补充
    • 代码简化:unordered_set可直接用nums迭代器初始化,省略循环插入;
    • 解法对比:迭代写法高效(O (1) 额外空间),递归写法更易体现分治思想(需辅助函数,O (n) 递归栈空间)。
相关推荐
charlie11451419130 分钟前
Cinux: 为大内核铺路
开发语言·c++·操作系统·现代c++
Jerry1 小时前
LeetCode 101. 对称二叉树
算法
hengcaib3 小时前
防封号电销外包公司怎么选合规线路与风控核查方法
笔记
可编程芯片开发3 小时前
基于MPPT最大功率跟踪的离网光伏发电系统Simulink建模与仿真
算法
AI科技星3 小时前
线性算子不是空间映射函数,是全域双螺旋场之间拉伸、旋转、耦合、坍缩的跨空间标准化变换载体《全域数学vs传统数学:人类文明进阶200讲》第80讲
线性代数·算法·矩阵·数据挖掘·回归·乖乖数学·全域数学
米罗篮3 小时前
矩阵快速幂 (Exponentiation By Squaring Applied To Matrices)
c++·线性代数·算法·矩阵
肖爱Kun3 小时前
C++设计策略模式
开发语言·c++·策略模式
dream_home84073 小时前
图像算法模型NPU适配与算法服务实战指南
人工智能·python·算法·npu 图像服务
胡渠洋3 小时前
postman学习
学习·测试工具·postman
大鱼>4 小时前
多宠物家庭智能管理平台:云端架构与多设备协同实战
python·算法·iot·宠物