【算法】1171.从链表中删去总和值为零的连续节点--通俗讲解

54 阅读4分钟

一、题目是啥?一句话说清

给定一个链表,反复删除其中和为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

  1. 添加虚拟头节点:0 → 1 → 2 → -3 → 3 → 1
  2. 计算前缀和
    • 节点0:前缀和=0
    • 节点1:前缀和=1
    • 节点2:前缀和=3
    • 节点-3:前缀和=0(与节点0的前缀和相同)
  3. 删除操作
    • 发现节点0和节点-3的前缀和都是0
    • 说明节点0.next到节点-3之间的节点和为0(1+2-3=0)
    • 将节点0.next指向节点-3.next
  4. 结果链表: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的序列。