1 题目
给你一个链表的头节点 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 <= 500 <= 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 的节点)来模拟执行过程,帮你理解递归的 "拆解 - 回溯" 过程:
- 递归拆解阶段 :从头节点开始,不断调用
removeElements处理下一个节点,直到遇到NULL(终止条件)。- 处理节点 1 → 调用处理节点 2 → 调用处理节点 6 → 调用处理节点 3 → 调用处理节点 6 → 调用处理 NULL(返回 NULL)。
- 回溯处理阶段 (从最后一个节点往回处理):
- 处理节点 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;
- 处理节点 6:
- 最终结果:链表变成 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 为空只是 "递" 的结束,"归" 才刚开始 |
递归的两个铁律(必须记死)
- 终止条件(base case):是 "递" 的刹车,不是递归的终点(比如 NULL 就是刹车,让传话停住);
- 递推关系:把 "我的活" 拆成 "手下的活 + 我的收尾活"(比如 "删 1→6→3 的 6"="删 6→3 的 6" + "判断 1 要不要删")。
新手最易上手的递归学习方法
- 先写终止条件 :不管逻辑多复杂,先写
if (head == NULL) return NULL;,避免栈溢出; - 用 3 个节点的小例子手动走一遍:像上面那样,写清楚 "谁喊谁、谁返回谁、谁被删",比看 100 遍代码管用;
- 忘掉迭代思路:学递归时别总想 "怎么遍历、怎么连接",就想 "怎么把活交给手下,自己只干最后一步"。
3 小结
对比
迭代版(你的思路)
- 为什么用虚拟头节点?因为如果原头节点的值就是要删的(比如链表是 6->1->2,val=6),直接操作原头节点会很麻烦;虚拟头节点让 "删除头节点" 和 "删除中间节点" 的逻辑完全一致,不用单独处理。
- 为什么遍历指针
cur从虚拟头开始?对应你说的 "比较当前 cur 结点的下一个指针",这样能方便地修改下一个节点的连接关系(删除操作)。
递归版(原 C 代码思路)
- 为什么能直接返回 head?递归的本质是 "自底向上" 处理:先把后面的链表都处理好(删掉所有 val),再判断当前 head 要不要删。比如原头节点要删,递归会返回
head->next(即第二个有效节点),最终的返回值就是新的头节点。
虽然这题是easy,也就在考察链表删除,但是迭代和递归真的要花心思,扎扎实实学会!至少我现在会迭代了!