一、题目是啥?一句话说清
给定一个链表,反复删除其中和为0的连续节点序列,直到不存在这样的序列为止。
示例:
- 输入:1 → 2 → -3 → 3 → 1
- 输出:3 → 1(因为1+2-3=0,所以删除前三个节点)
二、解题核心
使用前缀和和哈希表:计算累加和,如果某个累加和重复出现,说明这两个位置之间的节点和为0,可以删除。
这就像记账时,如果某两个时间点的余额相同,说明中间这段时间的收支总和为0,可以把这段时间的记录删除。
三、关键在哪里?(3个核心点)
想理解并解决这道题,必须抓住以下三个关键点:
1. 前缀和的概念
- 是什么:从链表头到当前节点的所有节点值的累加和。
- 为什么重要:如果两个节点的前缀和相同,说明这两个节点之间的节点值总和为0。
2. 哈希表的运用
- 是什么:用哈希表存储前缀和和对应的节点,当遇到相同前缀和时,可以快速找到对应的节点。
- 为什么重要:这样可以在O(1)时间内判断是否存在和为0的连续序列,并快速进行删除操作。
3. 虚拟头节点的使用
- 是什么:在链表头部添加一个值为0的虚拟节点。
- 为什么重要:可以统一处理删除头节点的情况,简化代码逻辑。
四、看图理解流程(通俗理解版本)
假设链表为:1 → 2 → -3 → 3 → 1
- 添加虚拟头节点:0 → 1 → 2 → -3 → 3 → 1
- 计算前缀和:
- 节点0:前缀和=0
- 节点1:前缀和=1
- 节点2:前缀和=3
- 节点-3:前缀和=0(与节点0的前缀和相同)
- 删除操作:
- 发现节点0和节点-3的前缀和都是0
- 说明节点0.next到节点-3之间的节点和为0(1+2-3=0)
- 将节点0.next指向节点-3.next
- 结果链表:0 → 3 → 1(去掉虚拟头节点后为3 → 1)
五、C++ 代码实现(附详细注释)
#include <iostream>
#include <unordered_map>
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* removeZeroSumSublists(ListNode* head) {
// 创建虚拟头节点,简化边界情况处理
ListNode* dummy = new ListNode(0);
dummy->next = head;
unordered_map<int, ListNode*> prefixSumMap;
int prefixSum = 0;
ListNode* current = dummy;
// 第一次遍历:记录每个前缀和对应的最后一个节点
while (current != nullptr) {
prefixSum += current->val;
prefixSumMap[prefixSum] = current; // 记录或更新前缀和对应的节点
current = current->next;
}
// 第二次遍历:删除和为0的连续节点
prefixSum = 0;
current = dummy;
while (current != nullptr) {
prefixSum += current->val;
// 如果当前前缀和在map中存在,说明这两个节点之间的和为0
if (prefixSumMap.find(prefixSum) != prefixSumMap.end()) {
// 跳过中间和为0的节点序列
current->next = prefixSumMap[prefixSum]->next;
}
current = current->next;
}
return dummy->next;
}
};
// 辅助函数:打印链表
void printList(ListNode* head) {
while (head != nullptr) {
cout << head->val << " ";
head = head->next;
}
cout << endl;
}
// 测试代码
int main() {
// 构建示例链表:1->2->-3->3->1
ListNode* head = new ListNode(1);
head->next = new ListNode(2);
head->next->next = new ListNode(-3);
head->next->next->next = new ListNode(3);
head->next->next->next->next = new ListNode(1);
Solution solution;
ListNode* result = solution.removeZeroSumSublists(head);
printList(result); // 输出:3 1
// 释放内存
while (result != nullptr) {
ListNode* temp = result;
result = result->next;
delete temp;
}
return 0;
}
六、时间空间复杂度
- 时间复杂度:O(n),其中n是链表长度。需要遍历链表两次。
- 空间复杂度:O(n),用于存储前缀和哈希表,最坏情况下需要存储n个键值对。
七、注意事项
- 虚拟头节点:必须使用虚拟头节点,因为可能删除头节点本身。
- 哈希表更新:哈希表中存储的是每个前缀和最后一次出现的位置,这样确保删除的是最大的连续和为0的序列。
- 重复删除:算法会自动处理需要多次删除的情况,因为第二次遍历时会处理所有和为0的序列。
- 边界情况:处理空链表、所有节点和为0、没有和为0的序列等情况。
- 内存管理:在C++中,被删除的节点需要正确释放内存,避免内存泄漏。
- 前缀和为0:虚拟头节点的前缀和为0,可以处理从链表头开始的和为0的序列。