一、题目是啥?一句话说清
给定一个链表和一个整数值,删除链表中所有值等于该整数的节点,并返回新的头节点。
示例:
- 输入:1 → 2 → 6 → 3 → 4 → 5 → 6, val = 6
- 输出:1 → 2 → 3 → 4 → 5
二、解题核心
使用虚拟头节点简化操作,遍历链表,当遇到值等于目标值的节点时,跳过该节点(即修改前一个节点的next指针)。
这就像在一条队伍中,我们要移除所有穿红色衣服的人,我们只需要让每个人记住他后面的人是谁,当遇到穿红衣服的人时,就直接记住他后面的人,跳过这个穿红衣服的人。
三、关键在哪里?(3个核心点)
想理解并解决这道题,必须抓住以下三个关键点:
1. 虚拟头节点的使用
- 是什么:创建一个虚拟头节点,其next指向原链表的头节点,这样可以简化删除头节点的操作。
- 为什么重要:因为头节点也可能需要被删除,使用虚拟头节点可以避免处理特殊的头部删除情况,使代码更统一和简洁。
2. 指针的遍历和跳过
- 是什么:遍历链表,当当前节点的值等于目标值时,修改前一个节点的next指针,跳过当前节点。
- 为什么重要:这是删除节点的核心操作,需要正确更新指针,确保链表不断裂,同时跳过所有值等于目标值的节点。
3. 内存管理(在C++中)
- 是什么:在删除节点时,需要释放被删除节点的内存,避免内存泄漏。
- 为什么重要:良好的内存管理是C++编程的基本要求,避免内存泄漏可以提高程序的稳定性和效率。
四、看图理解流程(通俗理解版本)
假设链表为:1 → 2 → 6 → 3 → 4 → 5 → 6,要删除值为6的节点。
-
初始化:
- 创建虚拟头节点dummy,dummy.next指向1。
- 设置prev指针指向dummy,curr指针指向1。
-
遍历和删除:
- 检查curr的值(1)≠6,prev和curr都前进:prev指向1,curr指向2。
- 检查curr的值(2)≠6,prev和curr都前进:prev指向2,curr指向6。
- 检查curr的值(6)=6,执行删除:
- 将prev的next指向curr的next(即2的next指向3)。
- 删除curr节点(释放内存)。
- curr指向3(prev的next)。
- 检查curr的值(3)≠6,prev和curr都前进:prev指向3,curr指向4。
- 检查curr的值(4)≠6,prev和curr都前进:prev指向4,curr指向5。
- 检查curr的值(5)≠6,prev和curr都前进:prev指向5,curr指向6。
- 检查curr的值(6)=6,执行删除:
- 将prev的next指向curr的next(即5的next指向null)。
- 删除curr节点(释放内存)。
- curr指向null。
- 遍历结束。
-
返回结果:返回dummy.next,即1 → 2 → 3 → 4 → 5。
五、C++ 代码实现(附详细注释)
#include <iostream>
using namespace std;
// 链表节点定义
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) {
// 创建虚拟头节点,简化操作
ListNode* dummy = new ListNode(0);
dummy->next = head;
ListNode* prev = dummy; // 前驱指针
ListNode* curr = head; // 当前指针
while (curr != nullptr) {
if (curr->val == val) {
// 找到需要删除的节点
prev->next = curr->next; // 跳过当前节点
delete curr; // 释放内存
curr = prev->next; // 更新当前指针
} else {
// 不需要删除,移动指针
prev = curr;
curr = curr->next;
}
}
ListNode* newHead = dummy->next;
delete dummy; // 释放虚拟头节点
return newHead;
}
};
// 辅助函数:打印链表
void printList(ListNode* head) {
while (head != nullptr) {
cout << head->val << " ";
head = head->next;
}
cout << endl;
}
// 测试代码
int main() {
// 创建示例链表:1->2->6->3->4->5->6
ListNode* head = new ListNode(1);
head->next = new ListNode(2);
head->next->next = new ListNode(6);
head->next->next->next = new ListNode(3);
head->next->next->next->next = new ListNode(4);
head->next->next->next->next->next = new ListNode(5);
head->next->next->next->next->next->next = new ListNode(6);
Solution solution;
ListNode* result = solution.removeElements(head, 6);
printList(result); // 输出:1 2 3 4 5
// 释放内存(实际应用中需要更完整的释放)
while (result != nullptr) {
ListNode* temp = result;
result = result->next;
delete temp;
}
return 0;
}
六、时间空间复杂度
- 时间复杂度:O(n),其中n是链表长度。需要遍历整个链表一次。
- 空间复杂度:O(1),只使用了常数额外空间(几个指针),不包括虚拟头节点(但虚拟头节点也是常数空间)。
七、注意事项
- 虚拟头节点的使用:使用虚拟头节点可以简化删除头节点的操作,避免特殊处理。
- 内存管理:在C++中,删除节点时需要释放内存,避免内存泄漏。但要注意,在删除节点后,需要更新指针,避免访问已释放的内存。
- 指针更新:在删除节点时,需要正确更新前驱指针的next指针,确保链表不断裂。
- 边界情况:处理空链表的情况,以及链表所有节点都需要删除的情况。
- 代码健壮性:确保在删除节点后,程序能正确处理后续节点,不会出现空指针异常。
** 算法通俗讲解推荐阅读**
【算法--链表】83.删除排序链表中的重复元素--通俗讲解
【算法--链表】删除排序链表中的重复元素 II--通俗讲解
【算法--链表】86.分割链表--通俗讲解
【算法】92.翻转链表Ⅱ--通俗讲解
【算法--链表】109.有序链表转换二叉搜索树--通俗讲解
【算法--链表】114.二叉树展开为链表--通俗讲解
【算法--链表】116.填充每个节点的下一个右侧节点指针--通俗讲解
【算法--链表】117.填充每个节点的下一个右侧节点指针Ⅱ--通俗讲解
【算法--链表】138.随机链表的复制--通俗讲解
【算法】143.重排链表--通俗讲解
【算法--链表】146.LRU缓存--通俗讲解
【算法--链表】147.对链表进行插入排序--通俗讲解
【算法】【链表】148.排序链表--通俗讲解
【算法】【链表】160.相交链表--通俗讲解