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) 递归栈空间)。
相关推荐
阳光九叶草LXGZXJ1 天前
达梦数据库-学习-50-分区表指定分区清理空洞率(交换分区方式)
linux·运维·数据库·sql·学习
2301_765703141 天前
C++代码复杂度控制
开发语言·c++·算法
m0_708830961 天前
C++中的享元模式实战
开发语言·c++·算法
naruto_lnq1 天前
分布式计算C++库
开发语言·c++·算法
dalong101 天前
A11:plus 控件窗口绘图基础
笔记·aardio
慎独4131 天前
重置学习系统:唤醒孩子的“双引擎”学习力
学习
m0_706653231 天前
模板编译期排序算法
开发语言·c++·算法
历程里程碑1 天前
Linxu14 进程一
linux·c语言·开发语言·数据结构·c++·笔记·算法
木井巳1 天前
【递归算法】验证二叉搜索树
java·算法·leetcode·深度优先·剪枝
m0_561359671 天前
嵌入式C++加密库
开发语言·c++·算法