1171. Remove Zero Sum Consecutive Nodes from Linked List
Problem Description
You are given the head of a linked list. Your task is to repeatedly delete consecutive sequences of nodes whose values sum to 0
until no such sequences remain in the list.
For example, if you have a linked list 1 -> 2 -> -3 -> 3 -> 1
, you can identify that the sequence 2 -> -3
sums to -1
, and the sequence 3 -> -3
sums to 0
. After removing the sequence that sums to 0
, you would have 1 -> 2 -> 1
. If there were another sequence summing to 0
, you would continue removing it.
The process continues until there are no more consecutive sequences of nodes that sum to 0
. You need to return the head of the modified linked list after all such removals.
The key insight is that when you find a sequence of nodes that sums to 0
, you can remove all those nodes by connecting the node before the sequence directly to the node after the sequence. The solution uses prefix sums to efficiently identify these zero-sum sequences - if two positions in the linked list have the same prefix sum, it means all nodes between them sum to 0
and can be removed.
Intuition
The key observation is that if we calculate the cumulative sum (prefix sum) while traversing the linked list, and we encounter the same prefix sum value at two different positions, it means the nodes between these two positions sum to 0
.
Why? Let's say at position i
we have prefix sum S
, and at position j
(where j > i
) we also have prefix sum S
. This means:
- Sum from start to position
i
=S
- Sum from start to position
j
=S
- Therefore, sum from position
i+1
to positionj
=S - S = 0
For example, consider the list 1 -> 2 -> -3 -> 3 -> 1
:
- At the dummy node (before 1): prefix sum =
0
- After node 1: prefix sum =
1
- After node 2: prefix sum =
3
- After node -3: prefix sum =
0
(we've seen this before!) - This tells us nodes between the first
0
and second0
(which are1 -> 2 -> -3
) sum to0
The clever part is using a hash table to store the last occurrence of each prefix sum along with its corresponding node. When we see a prefix sum that already exists, we know we can skip all nodes between the current position and the last occurrence of this sum.
By using two passes through the list:
- First pass: Record the last node for each prefix sum value
- Second pass: For each prefix sum, directly connect to the node after the last occurrence of that sum, effectively removing all zero-sum sequences
This approach ensures we remove all possible zero-sum consecutive sequences in just two traversals of the linked list.
Learn more about Linked List patterns.
Solution Approach
The solution uses prefix sum combined with a hash table to efficiently identify and remove zero-sum consecutive sequences.
Step 1: Setup
- Create a dummy node pointing to the head of the linked list. This helps handle edge cases where the entire list might sum to
0
. - Initialize a hash table
last
to store the mapping of prefix sum to the corresponding node.
Step 2: First Pass - Build Prefix Sum Map
s, cur = 0, dummy
while cur:
s += cur.val
last[s] = cur
cur = cur.next
- Traverse the linked list while maintaining a running sum
s
. - For each node, add its value to the cumulative sum.
- Store or update the mapping
last[s] = cur
, which records the last node that achieves this prefix sum. - If the same prefix sum appears multiple times, the later occurrence overwrites the earlier one in the hash table.
Step 3: Second Pass - Remove Zero-Sum Sequences
s, cur = 0, dummy
while cur:
s += cur.val
cur.next = last[s].next
cur = cur.next
- Traverse the linked list again with the same prefix sum calculation.
- For each node with prefix sum
s
, directly connect it tolast[s].next
. - This effectively skips all nodes between the current position and the last occurrence of this prefix sum, removing any zero-sum sequences.
Why This Works:
- If nodes between position
i
and positionj
sum to0
, they will have the same prefix sum. - In the first pass,
last[s]
stores the rightmost node with prefix sums
. - In the second pass, when we encounter prefix sum
s
at an earlier position, we skip directly to the node afterlast[s]
, removing all intermediate nodes that sum to0
.
Example Walkthrough:
For list 1 -> 2 -> -3 -> 3 -> 1
:
- First pass builds:
{0: dummy, 1: node(1), 3: node(2), 0: node(-3), 3: node(3), 4: node(1)}
- After overwriting:
{0: node(-3), 1: node(1), 3: node(3), 4: node(1)}
- Second pass: When we reach dummy with sum
0
, we connect it tolast[0].next
which isnode(3)
, effectively removing1 -> 2 -> -3
.
The time complexity is O(n)
where n
is the number of nodes, and space complexity is O(n)
for the hash table.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with the linked list: 1 -> 2 -> -3 -> 3 -> 1
Initial Setup:
- Create dummy node (value 0) pointing to head:
dummy(0) -> 1 -> 2 -> -3 -> 3 -> 1
- Initialize empty hash table
last = {}
First Pass - Building the Prefix Sum Map:
Starting from dummy, calculate prefix sums and store in hash table:
Current Node | Node Value | Prefix Sum | Action in Hash Table |
---|---|---|---|
dummy | 0 | 0 | last[0] = dummy |
node(1) | 1 | 0 + 1 = 1 | last[1] = node(1) |
node(2) | 2 | 1 + 2 = 3 | last[3] = node(2) |
node(-3) | -3 | 3 + (-3) = 0 | last[0] = node(-3) (overwrites dummy) |
node(3) | 3 | 0 + 3 = 3 | last[3] = node(3) (overwrites node(2)) |
node(1) | 1 | 3 + 1 = 4 | last[4] = node(1) |
Final hash table: {0: node(-3), 1: node(1), 3: node(3), 4: node(1)}
Second Pass - Removing Zero-Sum Sequences:
Traverse again from dummy, using the hash table to skip zero-sum sequences:
-
At dummy (sum = 0):
- Look up last[0] = node(-3)
- Set dummy.next = last[0].next = node(-3).next = node(3)
- This removes nodes 1 -> 2 -> -3 (they sum to 0!)
-
At node(3) (sum = 0 + 3 = 3):
- Look up last[3] = node(3) (itself)
- Set node(3).next = last[3].next = node(3).next = node(1)
- No change needed (already pointing correctly)
-
At node(1) (sum = 3 + 1 = 4):
- Look up last[4] = node(1) (itself)
- Set node(1).next = last[4].next = node(1).next = None
- Already at the end
Result: dummy -> 3 -> 1
The final linked list is 3 -> 1
(return dummy.next)
The beauty of this approach is that when we found prefix sum 0 at node(-3), we knew that everything from dummy to node(-3) summed to 0. By connecting dummy directly to what comes after node(-3), we removed the entire zero-sum sequence in one operation!
Solution Implementation
1# Definition for singly-linked list.
2# class ListNode:
3# def __init__(self, val=0, next=None):
4# self.val = val
5# self.next = next
6
7from typing import Optional
8
9class Solution:
10 def removeZeroSumSublists(self, head: Optional[ListNode]) -> Optional[ListNode]:
11 # Create dummy node to handle edge cases where head might be removed
12 dummy_node = ListNode(0)
13 dummy_node.next = head
14
15 # Dictionary to store the last node for each prefix sum
16 prefix_sum_to_node = {}
17
18 # First pass: calculate prefix sums and store the last occurrence of each sum
19 prefix_sum = 0
20 current_node = dummy_node
21
22 while current_node:
23 prefix_sum += current_node.val
24 # Store the last node that achieves this prefix sum
25 prefix_sum_to_node[prefix_sum] = current_node
26 current_node = current_node.next
27
28 # Second pass: connect nodes to skip zero-sum sublists
29 prefix_sum = 0
30 current_node = dummy_node
31
32 while current_node:
33 prefix_sum += current_node.val
34 # Connect current node directly to the node after the last occurrence
35 # of this prefix sum, effectively removing zero-sum sublists
36 current_node.next = prefix_sum_to_node[prefix_sum].next
37 current_node = current_node.next
38
39 return dummy_node.next
40
1/**
2 * Definition for singly-linked list.
3 * public class ListNode {
4 * int val;
5 * ListNode next;
6 * ListNode() {}
7 * ListNode(int val) { this.val = val; }
8 * ListNode(int val, ListNode next) { this.val = val; this.next = next; }
9 * }
10 */
11class Solution {
12 public ListNode removeZeroSumSublists(ListNode head) {
13 // Create a dummy node pointing to head to handle edge cases
14 ListNode dummy = new ListNode(0, head);
15
16 // HashMap to store the last node for each prefix sum
17 Map<Integer, ListNode> prefixSumToNode = new HashMap<>();
18
19 // First pass: Build the map of prefix sum to the last node with that sum
20 int prefixSum = 0;
21 ListNode current = dummy;
22
23 while (current != null) {
24 prefixSum += current.val;
25 // Store or update the last node for this prefix sum
26 prefixSumToNode.put(prefixSum, current);
27 current = current.next;
28 }
29
30 // Second pass: Connect nodes to skip zero-sum sublists
31 prefixSum = 0;
32 current = dummy;
33
34 while (current != null) {
35 prefixSum += current.val;
36 // Connect current node to the node after the last occurrence of this prefix sum
37 // This effectively removes all nodes that sum to zero between them
38 current.next = prefixSumToNode.get(prefixSum).next;
39 current = current.next;
40 }
41
42 // Return the head of the modified list (skip dummy node)
43 return dummy.next;
44 }
45}
46
1/**
2 * Definition for singly-linked list.
3 * struct ListNode {
4 * int val;
5 * ListNode *next;
6 * ListNode() : val(0), next(nullptr) {}
7 * ListNode(int x) : val(x), next(nullptr) {}
8 * ListNode(int x, ListNode *next) : val(x), next(next) {}
9 * };
10 */
11class Solution {
12public:
13 ListNode* removeZeroSumSublists(ListNode* head) {
14 // Create a dummy node pointing to head to handle edge cases
15 ListNode* dummyNode = new ListNode(0, head);
16
17 // Map to store the last occurrence of each prefix sum
18 unordered_map<int, ListNode*> prefixSumToNode;
19
20 // First pass: Calculate prefix sums and store the last node for each sum
21 ListNode* currentNode = dummyNode;
22 int prefixSum = 0;
23
24 while (currentNode != nullptr) {
25 prefixSum += currentNode->val;
26 // Update the map with the current prefix sum and node
27 // If the same sum appears again, it overwrites the previous entry
28 prefixSumToNode[prefixSum] = currentNode;
29 currentNode = currentNode->next;
30 }
31
32 // Second pass: Rebuild the list by skipping zero-sum sublists
33 prefixSum = 0;
34 currentNode = dummyNode;
35
36 while (currentNode != nullptr) {
37 prefixSum += currentNode->val;
38 // Connect current node directly to the node after the last occurrence
39 // of the same prefix sum, effectively removing zero-sum sublists
40 currentNode->next = prefixSumToNode[prefixSum]->next;
41 currentNode = currentNode->next;
42 }
43
44 // Return the modified list, skipping the dummy node
45 return dummyNode->next;
46 }
47};
48
1/**
2 * Definition for singly-linked list.
3 * class ListNode {
4 * val: number
5 * next: ListNode | null
6 * constructor(val?: number, next?: ListNode | null) {
7 * this.val = (val===undefined ? 0 : val)
8 * this.next = (next===undefined ? null : next)
9 * }
10 * }
11 */
12
13/**
14 * Removes all zero-sum consecutive sequences from a linked list
15 * @param head - The head of the linked list
16 * @returns The head of the modified linked list with zero-sum sublists removed
17 */
18function removeZeroSumSublists(head: ListNode | null): ListNode | null {
19 // Create a dummy node pointing to the head to handle edge cases
20 const dummyNode: ListNode = new ListNode(0, head);
21
22 // Map to store the last node for each prefix sum value
23 const prefixSumToNode: Map<number, ListNode> = new Map<number, ListNode>();
24
25 // First pass: Calculate prefix sums and store the last node for each sum
26 let prefixSum: number = 0;
27 for (let currentNode: ListNode | null = dummyNode; currentNode !== null; currentNode = currentNode.next) {
28 prefixSum += currentNode.val;
29 // Store or update the last node that has this prefix sum
30 prefixSumToNode.set(prefixSum, currentNode);
31 }
32
33 // Second pass: Skip zero-sum sublists by connecting nodes with same prefix sum
34 prefixSum = 0;
35 for (let currentNode: ListNode | null = dummyNode; currentNode !== null; currentNode = currentNode.next) {
36 prefixSum += currentNode.val;
37 // Connect current node directly to the node after the last occurrence of this prefix sum
38 // This effectively removes all nodes between them (which sum to zero)
39 currentNode.next = prefixSumToNode.get(prefixSum)!.next;
40 }
41
42 // Return the actual head (skip the dummy node)
43 return dummyNode.next;
44}
45
Time and Space Complexity
Time Complexity: O(n)
The algorithm makes two passes through the linked list:
- First pass: Traverses the entire linked list once to build the prefix sum hash map, visiting each node exactly once. This takes
O(n)
time. - Second pass: Traverses the linked list again to remove zero-sum sublists by updating the
next
pointers based on the hash map. Each node is visited exactly once. This takesO(n)
time.
Total time complexity: O(n) + O(n) = O(n)
, where n
is the number of nodes in the linked list.
Space Complexity: O(n)
The algorithm uses additional space for:
- Hash map
last
: In the worst case where no sublists sum to zero, the hash map stores a unique prefix sum for each node in the linked list, requiringO(n)
space. - Variables
s
,cur
, anddummy
: These use constant spaceO(1)
.
Total space complexity: O(n)
, where n
is the number of nodes in the linked list.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Not Using a Dummy Node
Problem: Without a dummy node, handling cases where the entire list or its beginning sums to 0 becomes complicated.
For example, if the list is 3 -> -3 -> 1
, the first two nodes sum to 0 and should be removed, leaving just 1
. Without a dummy node, you'd need special logic to update the head pointer.
Solution: Always use a dummy node with value 0 pointing to the head. This ensures:
- The prefix sum starts from a known state
- You never have to special-case updating the head pointer
- The dummy node acts as an anchor that won't be removed
Pitfall 2: Attempting to Remove Nodes During First Pass
Problem: Some might try to remove zero-sum sequences immediately when detected during the first traversal:
# Incorrect approach
prefix_sum_to_node = {}
current = dummy
prefix_sum = 0
while current:
prefix_sum += current.val
if prefix_sum in prefix_sum_to_node:
# Try to remove nodes immediately - THIS IS WRONG
prefix_sum_to_node[prefix_sum].next = current.next
else:
prefix_sum_to_node[prefix_sum] = current
current = current.next
This fails because:
- It doesn't handle nested or overlapping zero-sum sequences correctly
- Example:
1 -> 2 -> -3 -> 3 -> -2 -> -1
has multiple overlapping zero-sum sequences - Removing during traversal can skip nodes that should be processed
Solution: Use two separate passes:
- First pass: Build the complete prefix sum map (later occurrences overwrite earlier ones)
- Second pass: Use the map to reconnect nodes properly
Pitfall 3: Not Handling the Overwriting Behavior Correctly
Problem: Misunderstanding why we need to overwrite earlier occurrences of the same prefix sum in the hash map.
Consider the list: 1 -> 2 -> -3 -> 4 -> -4
- Prefix sums:
0 -> 1 -> 3 -> 0 -> 4 -> 0
- If we kept the first occurrence of prefix sum 0 (at the dummy), we'd miss removing the later zero-sum sequences
Solution: Always update prefix_sum_to_node[prefix_sum] = current
even if the key exists. This ensures we keep the rightmost occurrence of each prefix sum, which allows us to remove the maximum number of nodes that form zero-sum sequences.
Pitfall 4: Forgetting to Return dummy.next
Instead of dummy
Problem: Returning the dummy node itself instead of the actual head of the modified list.
# Wrong
return dummy_node
# Correct
return dummy_node.next
Solution: Always remember that the dummy node is just a helper. The actual list starts at dummy_node.next
.
Which of the following problems can be solved with backtracking (select multiple)
Recommended Readings
Linked List Cycle Given a linked list with potentially a loop determine whether the linked list from the first node contains a cycle in it For bonus points do this with constant space Parameters nodes The first node of a linked list with potentially a loop Result Whether there is a loop contained
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!