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) 递归栈空间)。
相关推荐
tingshuo29179 小时前
S001 【模板】从前缀函数到KMP应用 字符串匹配 字符串周期
笔记
董董灿是个攻城狮10 小时前
AI视觉连载8:传统 CV 之边缘检测
算法
blasit17 小时前
笔记:Qt C++建立子线程做一个socket TCP常连接通信
c++·qt·tcp/ip
AI软著研究员17 小时前
程序员必看:软著不是“面子工程”,是代码的“法律保险”
算法
FunnySaltyFish17 小时前
什么?Compose 把 GapBuffer 换成了 LinkBuffer?
算法·kotlin·android jetpack
颜酱18 小时前
理解二叉树最近公共祖先(LCA):从基础到变种解析
javascript·后端·算法
地平线开发者1 天前
SparseDrive 模型导出与性能优化实战
算法·自动驾驶
董董灿是个攻城狮1 天前
大模型连载2:初步认识 tokenizer 的过程
算法
地平线开发者1 天前
地平线 VP 接口工程实践(一):hbVPRoiResize 接口功能、使用约束与典型问题总结
算法·自动驾驶
罗西的思考1 天前
AI Agent框架探秘:拆解 OpenHands(10)--- Runtime
人工智能·算法·机器学习