移除链表元素 图解题解
这道题到底在问什么
- 输入
- head = 6→6→1→2→6→3→4→6, val = 6
- 输出
- 1→2→3→4
最优解:一步一步想明白
- 3「pre 跟在 cur 后面一步」是关键:删 cur 时要靠 pre.next 接到 cur 的下一个,所以必须有个「前驱」指针随时待命。
- 4已经接好哑结点:dummy→6→6→1→2→6→3→4→6。pre 停在 dummy,cur 停在头节点 6。要删的值 val = 6。下面 cur 一格一格往后走,pre 紧跟其后。注意头节点也是 6,待会儿要靠哑结点把它删掉。
- 5pre 在 哑结点,cur 在 6。问:cur 的值 6 等于要删的 6 吗?相等 → 命中!下一帧用 pre.next 跨过它,把它摘掉。
- 6执行 pre.next = cur.next:把这个 6 摘掉(变灰、两侧箭头断成 ·),pre 留在原地不动(哑结点),cur 前进到 6。pre 不动是因为它后面接上的新节点还没检查过。
- 7pre 在 哑结点,cur 在 6。问:cur 的值 6 等于要删的 6 吗?相等 → 命中!下一帧用 pre.next 跨过它,把它摘掉。
- 8执行 pre.next = cur.next:把这个 6 摘掉(变灰、两侧箭头断成 ·),pre 留在原地不动(哑结点),cur 前进到 1。pre 不动是因为它后面接上的新节点还没检查过。
- 9pre 在 哑结点,cur 在 1。问:cur 的值 1 等于要删的 6 吗?不相等 → 这个节点保留,下一帧 pre 跟上来。
- 10执行 pre = cur、cur = cur.next:节点 1 确认保留(变绿),pre 跟到它上面,cur 前进到 2。pre 永远紧跟在 cur 前一格。
- 11pre 在 1,cur 在 2。问:cur 的值 2 等于要删的 6 吗?不相等 → 这个节点保留,下一帧 pre 跟上来。
- 12执行 pre = cur、cur = cur.next:节点 2 确认保留(变绿),pre 跟到它上面,cur 前进到 6。pre 永远紧跟在 cur 前一格。
- 13pre 在 2,cur 在 6。问:cur 的值 6 等于要删的 6 吗?相等 → 命中!下一帧用 pre.next 跨过它,把它摘掉。
- 14执行 pre.next = cur.next:把这个 6 摘掉(变灰、两侧箭头断成 ·),pre 留在原地不动(2),cur 前进到 3。pre 不动是因为它后面接上的新节点还没检查过。
- 15pre 在 2,cur 在 3。问:cur 的值 3 等于要删的 6 吗?不相等 → 这个节点保留,下一帧 pre 跟上来。
- 16执行 pre = cur、cur = cur.next:节点 3 确认保留(变绿),pre 跟到它上面,cur 前进到 4。pre 永远紧跟在 cur 前一格。
- 17pre 在 3,cur 在 4。问:cur 的值 4 等于要删的 6 吗?不相等 → 这个节点保留,下一帧 pre 跟上来。
- 18执行 pre = cur、cur = cur.next:节点 4 确认保留(变绿),pre 跟到它上面,cur 前进到 6。pre 永远紧跟在 cur 前一格。
- 19pre 在 4,cur 在 6。问:cur 的值 6 等于要删的 6 吗?相等 → 命中!下一帧用 pre.next 跨过它,把它摘掉。
- 20执行 pre.next = cur.next:把这个 6 摘掉(变灰、两侧箭头断成 ·),pre 留在原地不动(4),cur 前进到 空(链表末尾)。pre 不动是因为它后面接上的新节点还没检查过。
- 21cur 走到链表末尾(空),遍历结束。灰掉的 4 个 6 都被摘掉了。最后返回 dummy.next —— 不管原来的头删没删,dummy.next 永远指向真正的新头,结果是 1→2→3→4。
⚠️ 容易写错的地方
✗ 错:不用哑结点,直接从 head 删
✓ 对:在 head 前接哑结点 dummy
头节点本身可能要删(如 6→1,删 6),没哑结点就得写额外特判循环移动 head
✗ 错:命中删除后还让 pre = cur
✓ 对:命中时 pre 必须留在原地不动
pre.next 刚接上的新节点还没检查,pre 前移会漏检连续命中(如 …→6→6→…)
✗ 错:返回 head
✓ 对:返回 dummy.next
原 head 若被删,head 已是失效节点;dummy.next 才指向真正的新头
完整代码(Python / C++ / Java)
Python
def removeElements(head, val):
dummy = ListNode(0, head) # 哑结点接在头前
pre, cur = dummy, head
while cur:
if cur.val == val:
pre.next = cur.next # 跨过 cur,pre 不动
else:
pre = cur # 保留,pre 跟上
cur = cur.next
return dummy.nextC++
ListNode* removeElements(ListNode* head, int val){
ListNode dummy(0); dummy.next = head;
ListNode *pre = &dummy, *cur = head;
while (cur) {
if (cur->val == val) pre->next = cur->next;
else pre = cur;
cur = cur->next;
}
return dummy.next;
}Java
class Solution {
public ListNode removeElements(ListNode head, int val) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode pre = dummy, cur = head;
while (cur != null) {
if (cur.val == val) {
pre.next = cur.next; // 跨过 cur,pre 不动
} else {
pre = cur; // 保留,pre 跟上
}
cur = cur.next;
}
return dummy.next;
}
}复杂度
时间
O(n)
cur 把每个节点恰好经过一遍
空间
O(1)
只用 dummy/pre/cur 三个指针,原地改 next
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 移除链表元素 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
哑结点(dummy node)到底解决了什么问题?+
它让「删头节点」和「删中间节点」统一成同一种操作——都靠某个前驱节点的 .next 来跳过目标。没有哑结点时,删头节点没有前驱,必须单独写特判;接上哑结点后,头节点也有了前驱(dummy),代码就只剩一套逻辑,最后返回 dummy.next。
能用递归解吗?+
能。head.next = removeElements(head.next, val) 先处理后面;若 head.val == val 就返回 head.next(跳过自己),否则返回 head。简洁但有 O(n) 递归栈空间,不如迭代的 O(1)。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 移除链表元素 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。