Leetcode 97 移除链表元素

1 题目

203. 移除链表元素

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点

示例 1:

复制代码
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]

示例 2:

复制代码
输入:head = [], val = 1
输出:[]

示例 3:

复制代码
输入:head = [7,7,7,7], val = 7
输出:[]

提示:

  • 列表中的节点数目在范围 [0, 104]
  • 1 <= Node.val <= 50
  • 0 <= val <= 50

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* removeElements(ListNode* head, int val) {
        if(head == nullptr){
            return head;
        }

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

虽然简单,但是我又忘了!/(ㄒoㄒ)/,算法的难就难在这里吧,必须要重复做,不能不求甚解糊弄过去欺骗自己,代码是客观的。

看看25年10月写的...

c语言代码(递归)

cpp 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* removeElements(struct ListNode* head, int val) {
    if (head == NULL) {
        return head;
    }
    head -> next = removeElements(head->next, val);
    return head->val == val ? head->next : head;
}

1. 代码整体功能

这段代码的核心功能是:删除单链表中所有值等于给定 val 的节点 ,最后返回处理后的链表头节点。它采用了递归的方式来实现这个功能,是链表删除指定元素的经典递归解法。

2. 代码逐行拆解

先明确基础结构:struct ListNode 是单链表的节点定义,包含一个整数值 val 和指向下一个节点的指针 next

cpp 复制代码
struct ListNode* removeElements(struct ListNode* head, int val) {
    // 递归终止条件:如果当前节点是空(链表遍历完了),直接返回空
    if (head == NULL) {
        return head;
    }
    // 核心递归逻辑:先处理当前节点的下一个节点
    head -> next = removeElements(head->next, val);
    // 处理当前节点:如果当前节点值等于val,就跳过它(返回下一个节点);否则返回当前节点
    return head->val == val ? head->next : head;
}
关键逻辑解释

我们用一个例子(链表:1 -> 2 -> 6 -> 3 -> 6,要删除值为 6 的节点)来模拟执行过程,帮你理解递归的 "拆解 - 回溯" 过程:

  1. 递归拆解阶段 :从头节点开始,不断调用 removeElements 处理下一个节点,直到遇到 NULL(终止条件)。
    • 处理节点 1 → 调用处理节点 2 → 调用处理节点 6 → 调用处理节点 3 → 调用处理节点 6 → 调用处理 NULL(返回 NULL)。
  2. 回溯处理阶段 (从最后一个节点往回处理):
    • 处理节点 6:head->next 是 NULL,判断 6==6,返回 head->next(NULL),所以上一层的节点 3 的 next 变成 NULL;
    • 处理节点 3:head->next 是 NULL,判断 3!=6,返回节点 3,所以上一层的节点 6 的 next 变成 3;
    • 处理节点 6:head->next 是 3,判断 6==6,返回 3,所以上一层的节点 2 的 next 变成 3;
    • 处理节点 2:head->next 是 3,判断 2!=6,返回节点 2,所以上一层的节点 1 的 next 变成 2;
    • 处理节点 1:head->next 是 2,判断 1!=6,返回节点 1;
  3. 最终结果:链表变成 1 -> 2 -> 3,所有值为 6 的节点都被删除。

3. 补充说明

  • 递归的核心思想:把 "删除整个链表的指定节点" 拆解为 "删除当前节点的下一个链表的指定节点 + 判断当前节点是否需要删除"
  • 优势:代码简洁,无需额外处理头节点(递归天然处理了头节点可能被删除的情况);
  • 注意:递归会占用栈空间,如果链表过长(比如 10 万 + 节点),可能导致栈溢出,这种场景更适合用迭代(循环)实现。

用递归的思想,现在有了一点数据结构的学习基础,可以有更深的体会,当时的确是也没弄懂太明白。

说起来容易做起来难,自己一上手写c++的还是写不来,先梳理好思路 :

1.base case 空指针返回

2.整个流程 : 遍历 - 比较值 - 判断 - 删除

比较值注意是比较当前cur结点的下一个指针 , 如果这个值是要删掉的,跳过它,重新连接链表(删除操作)

以上是我自己的想法,可是为什么,c语言代码里面直接操作头指针,直接返回head结点?

解惑

你现在的困惑特别典型 ------ 自己梳理的迭代思路(遍历、比较下一个节点、重新连接)和递归代码里直接操作头指针、返回 head 的逻辑对不上,对吧?

我先帮你把这两种思路的核心差异讲透,再用你能理解的**"迭代版" 和 "递归版"**对比,你就能明白为什么递归代码可以直接操作 head 了。

原因:你梳理的是「迭代思路」,而原代码是「递归思路」

你总结的 "比较 cur 的下一个节点、跳过删除、重新连接",是迭代(循环) 处理链表的经典套路(新手最容易上手的方式);

而原 C 语言代码用的是递归,递归的核心是「把问题拆解给子问题」,所以不需要像迭代那样 "绕着下一个节点操作",而是直接对当前节点做判断,这就是它能直接操作 head 的原因。

c++代码(迭代)

cpp 复制代码
class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        // 1. base case:空链表直接返回
        if (head == nullptr) {
            return nullptr;
        }

        // 关键:创建虚拟头节点,避免单独处理头节点被删除的情况
        ListNode* dummyHead = new ListNode(0); // 虚拟头节点,值随便设
        dummyHead->next = head;                // 虚拟头指向原头节点
        ListNode* cur = dummyHead;             // 遍历指针,从虚拟头开始

        // 2. 遍历链表(你的核心思路:比较cur的下一个节点)
        while (cur->next != nullptr) {
            // 3. 比较值:如果下一个节点是要删的
            if (cur->next->val == val) {
                // 4. 删除操作:跳过这个节点
                ListNode* temp = cur->next;    // 暂存要删的节点,方便释放内存
                cur->next = cur->next->next;   // 重新连接链表
                delete temp;                   // C++ 要手动释放内存,避免泄漏
            } else {
                // 不是要删的,遍历指针后移
                cur = cur->next;
            }
        }

        // 注意:最终的头节点是虚拟头的下一个(原头可能被删了)
        head = dummyHead->next;
        delete dummyHead; // 释放虚拟头节点
        return head;
    }
};

写明白了!

再看看递归版本!

c++代码(递归)

cpp 复制代码
class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        // 1. base case:空指针返回
        if (head == nullptr) {
            return nullptr;
        }

        // 2. 递归处理下一个节点,把结果赋给当前节点的next
        head->next = removeElements(head->next, val);

        // 3. 判断当前节点是否要删:要删就返回next,否则返回自己
        if (head->val == val) {
            ListNode* temp = head;
            head = head->next;
            delete temp; // 释放要删除的节点
            return head;
        } else {
            return head;
        }
    }
};

我觉得这个递归的思路也许是这样理解:

base case 就不说了,递归处理其实就是把大任务分成小任务,就像对半锯木头一样,一直对新的最短的木头对半,直到被约束条件限制了就终止循环。

那这里我们其实也是这样做的,从head阶段开始一步一步,head - > next 在排队等候加入,如果是val,那么跳过,用temp删除掉 ; 如果不是val,加入,返回当前的head。

递归下去每个结点加入时,也成为了新的head,然后一个一个又继续往下寻找,一直到head是空的,结束了。


以上是错误的有偏差的思路,但是我也不删掉,以后自己能回头看看有哪里的认识是错的。


先定一个简单场景(所有例子都围绕它)

我们有一个链表:1 → 6 → 3(节点 1 是头),要删除所有值为 6 的节点。递归函数的目标:返回 "删掉 6 之后的新链表头"。

递归的本质:先让 "后面的人干完活,自己再干"

把每个节点想象成一个 "打工人",递归就是:

节点 1(包工头):"我不先干活,让我手下节点 6 先把它后面的活干完,干完了告诉我结果,我再处理我自己的事。"

节点 6(小工):"我也不先干活,让我手下节点 3 先把它后面的活干完,干完了告诉我结果,我再处理我自己的事。"

节点 3(临时工):"我也不先干活,让我手下 NULL 先干活...... 哦,NULL 没活干,那我先处理我自己的事。"

一步一步拆:递归的 "递" 和 "归"(附可视化)

递归分两个阶段:递(往下传话)归(往回交差),少一个都不行。

阶段 1:递(往下传话,只拆活,不干活)

核心动作:每个节点都喊自己的下一个节点 "先干活",直到喊到 NULL。

复制代码
包工头节点1 → 喊 → 小工节点6 → 喊 → 临时工节点3 → 喊 → NULL

具体代码对应:

复制代码
// 节点1执行:head->next = removeElements(节点6, 6) → 喊节点6干活
// 节点6执行:head->next = removeElements(节点3, 6) → 喊节点3干活
// 节点3执行:head->next = removeElements(NULL, 6) → 喊NULL干活
// NULL执行:触发base case,返回NULL → 传话结束,开始归
阶段 2:归(往回交差,才真正干活 / 删节点)

核心动作:从最后一个节点往回走,每个节点拿到手下的 "干活结果",再判断自己要不要被删。

步骤 1:NULL → 节点 3(临时工交差)
  • NULL 返回了 "没活干"(NULL),节点 3 把自己的 next 设为 NULL;

  • 节点 3 判断自己的 val=3≠6 → 不用删,返回自己(节点 3)给节点 6;

    节点3:"我没烂(不是6),我把结果(我自己)交给节点6!"

步骤 2:节点 3 → 节点 6(小工交差)
  • 节点 3 返回了 "节点 3",节点 6 把自己的 next 设为节点 3;

  • 节点 6 判断自己的 val=6=6 → 要删!删掉自己,返回节点 3 给节点 1;

    节点6:"我烂了(是6),我删掉自己,把节点3交给节点1!"

步骤 3:节点 6 → 节点 1(包工头交差)
  • 节点 6 返回了 "节点 3",节点 1 把自己的 next 设为节点 3;

  • 节点 1 判断自己的 val=1≠6 → 不用删,返回自己(节点 1);

    节点1:"我没烂,我把结果(我自己→节点3)交出去!"

最终结果

返回的头节点是 1,链表变成 1 → 3,节点 6 被删掉 ------ 这就是递归的完整过程。

对比你之前的理解:核心纠正

你之前的理解 真实的递归逻辑
head->next 排队等候加入 head->next 先干完所有活,再把结果交回来
节点加入时判断 / 删除 节点只有在 "归" 的阶段才判断 / 删除
head 为空就结束递归 head 为空只是 "递" 的结束,"归" 才刚开始

递归的两个铁律(必须记死)

  1. 终止条件(base case):是 "递" 的刹车,不是递归的终点(比如 NULL 就是刹车,让传话停住);
  2. 递推关系:把 "我的活" 拆成 "手下的活 + 我的收尾活"(比如 "删 1→6→3 的 6"="删 6→3 的 6" + "判断 1 要不要删")。

新手最易上手的递归学习方法

  1. 先写终止条件 :不管逻辑多复杂,先写 if (head == NULL) return NULL;,避免栈溢出;
  2. 用 3 个节点的小例子手动走一遍:像上面那样,写清楚 "谁喊谁、谁返回谁、谁被删",比看 100 遍代码管用;
  3. 忘掉迭代思路:学递归时别总想 "怎么遍历、怎么连接",就想 "怎么把活交给手下,自己只干最后一步"。

3 小结

对比

迭代版(你的思路)
  • 为什么用虚拟头节点?因为如果原头节点的值就是要删的(比如链表是 6->1->2,val=6),直接操作原头节点会很麻烦;虚拟头节点让 "删除头节点" 和 "删除中间节点" 的逻辑完全一致,不用单独处理。
  • 为什么遍历指针 cur 从虚拟头开始?对应你说的 "比较当前 cur 结点的下一个指针",这样能方便地修改下一个节点的连接关系(删除操作)。
递归版(原 C 代码思路)
  • 为什么能直接返回 head?递归的本质是 "自底向上" 处理:先把后面的链表都处理好(删掉所有 val),再判断当前 head 要不要删。比如原头节点要删,递归会返回 head->next(即第二个有效节点),最终的返回值就是新的头节点。

虽然这题是easy,也就在考察链表删除,但是迭代和递归真的要花心思,扎扎实实学会!至少我现在会迭代了!

相关推荐
Paul_09202 分钟前
golang编程题
开发语言·算法·golang
名字不相符3 分钟前
NSSCTF2026年1月8日每日一练之[第五空间 2021]WebFTP
学习·萌新
四谎真好看5 分钟前
JavaWeb 学习笔记(Day02)之Vue
笔记·学习·vue·学习笔记·javaweb
颜酱7 分钟前
用填充表格法-继续吃透完全背包及其变形
前端·后端·算法
:mnong8 分钟前
辅助学习神经网络
人工智能·神经网络·学习
夏秃然10 分钟前
打破预测与决策的孤岛:如何构建“能源垂类大模型”?
算法·ai·大模型
进阶小白猿11 分钟前
Java技术八股学习Day14
java·数据库·学习
南屿欣风12 分钟前
Sentinel 资源异常处理优先级笔记
spring boot·笔记·sentinel
ChoSeitaku14 分钟前
16.C++入门:list|手撕list|反向迭代器|与vector对比
c++·windows·list
氷泠14 分钟前
课程表系列(LeetCode 207 & 210 & 630 & 1462)
算法·leetcode·拓扑排序·反悔贪心·三色标记法