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) 递归栈空间)。
相关推荐
晚霞的不甘14 小时前
Flutter for OpenHarmony 可视化教学:A* 寻路算法的交互式演示
人工智能·算法·flutter·架构·开源·音视频
doupoa14 小时前
内存指针是什么?为什么指针还要有偏移量?
android·c++
望舒51314 小时前
代码随想录day25,回溯算法part4
java·数据结构·算法·leetcode
冉佳驹15 小时前
C++ ——— 异常处理的核心机制和智能指针管理
c++·异常捕获·异常继承体与多态·重载抛异常·raii思想·智能指针shared_ptr·weak_ptr指针
C++ 老炮儿的技术栈15 小时前
Qt 编写 TcpClient 程序 详细步骤
c语言·开发语言·数据库·c++·qt·算法
KYGALYX15 小时前
逻辑回归详解
算法·机器学习·逻辑回归
听麟15 小时前
HarmonyOS 6.0+ 跨端智慧政务服务平台开发实战:多端协同办理与电子证照管理落地
笔记·华为·wpf·音视频·harmonyos·政务
yuuki23323315 小时前
【C++】继承
开发语言·c++·windows
铉铉这波能秀15 小时前
LeetCode Hot100数据结构背景知识之集合(Set)Python2026新版
数据结构·python·算法·leetcode·哈希算法
参.商.15 小时前
【Day 27】121.买卖股票的最佳时机 122.买卖股票的最佳时机II
leetcode·golang