Remove Zero Sum Consecutive Nodes from Linked List
MedPrompt
Given the head of a linked list, we repeatedly delete consecutive sequences of nodes that sum to 0 until there are no such sequences.
After doing so, return the head of the final linked list. You may return any such answer.
(Note that in the examples below, all sequences are serializations of ListNode objects.)
Example 1:
Input: head = [1,2,-3,3,1]
Output: [3,1]
Note: The answer [1,2,1] would also be accepted.Example 2:
Input: head = [1,2,3,-3,4]
Output: [1,2,4]Example 3:
Input: head = [1,2,3,-3,-2]
Output: [1]
Constraints:
- The given linked list will contain between
1and1000nodes. - Each node in the linked list has
-1000 <= node.val <= 1000.
Approaches
3 approaches with complexity analysis and trade-offs.
This approach uses a straightforward, brute-force method with nested loops to find and remove zero-sum consecutive subsequences. It iterates through all possible consecutive sublists, calculates their sum, and removes them if the sum is zero. This process is repeated until no such sublists can be found.
Algorithm
- Create a
dummynode and setdummy.next = head. This simplifies handling cases where the head node is part of the sequence to be removed. - Initialize an
outerpointer to thedummynode. - Loop with the
outerpointer as long as it's not null. - Inside this loop, start an
innerloop with a pointerinnerstarting fromouter.nextand asuminitialized to 0. - The
innerloop traverses the rest of the list, accumulating the sum of node values. - If at any point
sumbecomes 0, it means the consecutive sequence of nodes fromouter.nexttoinnersums to zero. - Upon finding such a sequence, we remove it by setting
outer.next = inner.next. This effectively bypasses all the nodes in the zero-sum sequence. - After a deletion, we break the
innerloop and theouterloop continues from the sameouternode, but with itsnextpointer updated. This is crucial because the deletion might have formed a new zero-sum sequence with earlier nodes. - If the
innerloop completes without thesumever becoming 0, it means no zero-sum sequence starts atouter.next. In this case, we can safely advanceouterto its next node. - Finally, return
dummy.next, which will be the head of the modified list.
Walkthrough
The brute-force approach systematically checks every possible consecutive sequence of nodes for a zero sum. We use a dummy node pointing to the head to handle deletions at the beginning of the list gracefully.
We iterate with an outer pointer, which marks the node just before the start of a potential zero-sum sequence. For each position of outer, we use an inner pointer to traverse the rest of the list, calculating the sum of nodes from outer.next to inner.
If the sum becomes zero, we've found a sequence to delete. We do this by updating the outer.next pointer to inner.next, effectively cutting the zero-sum sequence out of the list. After a deletion, we must re-evaluate from the same outer node, as the new connection might form another zero-sum sequence. If the inner loop finishes without finding a zero-sum sequence, we advance the outer pointer.
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */class Solution { public ListNode removeZeroSumSublists(ListNode head) { ListNode dummy = new ListNode(0, head); ListNode outer = dummy; while (outer != null) { int prefixSum = 0; ListNode inner = outer.next; boolean found = false; while (inner != null) { prefixSum += inner.val; if (prefixSum == 0) { outer.next = inner.next; found = true; // Break inner loop and restart scan from the same outer node break; } inner = inner.next; } // Only advance outer if no deletion was made from its position if (!found) { outer = outer.next; } // If a deletion was made, outer stays put to re-check from its new .next } return dummy.next; }}Complexity
Time
O(N^2) - In the worst-case scenario (e.g., a list like `[1, 2, 3, ..., -sum]`), the outer loop runs N times, and for each, the inner loop might also run up to N times. This results in a quadratic time complexity.
Space
O(1) - We only use a few pointers (`dummy`, `outer`, `inner`) to traverse the list, so the space used is constant regardless of the list size.
Trade-offs
Pros
It's relatively easy to understand the logic.
It uses constant extra space, O(1), as it only requires a few pointers.
Cons
The time complexity of O(N^2) is inefficient for large linked lists and will be slow.
A more efficient approach uses a hash map to store the prefix sums encountered while traversing the linked list. The key idea is that if a prefix sum S is seen at node i and again at node j, the sum of the elements between i and j is zero. This allows for faster detection of zero-sum sequences. This specific version accomplishes the task in a single pass.
Algorithm
- Create a
dummynode with value 0 and point itsnextto thehead. - Initialize a
HashMap<Integer, ListNode>to store prefix sums and the corresponding nodes where they occur. Put an initial entry(0, dummy)into the map. - Initialize
prefixSum = 0. - Traverse the list with a
currentpointer, starting from thedummynode. - In each step, update the
prefixSumby adding thecurrentnode's value. - Check if the new
prefixSumalready exists in the map. a. If it does, it means the sublist between the node previously stored for thisprefixSum(prev = map.get(prefixSum)) and thecurrentnode sums to zero. b. Before deleting this sublist, we must clean the map. We iterate fromprev.nexttocurrent, removing the map entries for all the intermediate nodes, as their prefix sums are now invalid. c. After cleaning the map, we perform the deletion by settingprev.next = current.next. d. If theprefixSumis not in the map, we add a new entry:map.put(prefixSum, current). - Continue this process until the end of the list is reached.
- Return
dummy.next.
Walkthrough
This method traverses the list just once, building up a map of prefix sums to nodes as it goes. We use a dummy node to anchor the process, with an initial prefix sum of 0 pointing to it.
As we iterate through the list, we calculate the cumulative prefixSum. If we encounter a prefixSum that we've seen before, we know the nodes between the previous occurrence and the current node form a zero-sum sequence.
The main challenge in this one-pass approach is maintaining the correctness of the map. When we find a zero-sum sequence and are about to delete it, we must also remove the map entries corresponding to all the nodes being deleted. This is because their prefix sums are calculated based on a path that is now being removed. After cleaning the stale entries from the map, we can safely bypass the sequence by updating the next pointer of the previous node.
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */import java.util.HashMap;import java.util.Map; class Solution { public ListNode removeZeroSumSublists(ListNode head) { ListNode dummy = new ListNode(0, head); Map<Integer, ListNode> map = new HashMap<>(); int prefixSum = 0; // First, put the initial state in the map map.put(0, dummy); ListNode current = head; while (current != null) { prefixSum += current.val; if (map.containsKey(prefixSum)) { ListNode prev = map.get(prefixSum); ListNode toRemove = prev.next; int p = prefixSum + toRemove.val; // Remove stale entries from the map while (p != prefixSum) { map.remove(p); toRemove = toRemove.next; p += toRemove.val; } // Bypass the zero-sum sequence prev.next = current.next; } else { map.put(prefixSum, current); } current = current.next; } return dummy.next; }}Complexity
Time
O(N) - Although there is a nested loop to clean the map, each node is added to and removed from the map at most once. This leads to an amortized time complexity of O(N).
Space
O(N) - In the worst-case scenario where all prefix sums are unique, the hash map will store an entry for each of the N nodes.
Trade-offs
Pros
Highly efficient with an O(N) time complexity.
Processes the list in a single pass, which can be faster in practice than a two-pass approach.
Cons
The logic for cleaning up the hash map adds complexity to the implementation.
While amortized O(N), the operations within the loop can be costly if long zero-sum sequences are found repeatedly.
This optimal approach also uses a hash map and the concept of prefix sums but simplifies the logic by breaking the problem into two separate passes. The first pass is dedicated to building a map of prefix sums to nodes, and the second pass uses this map to efficiently rewire the linked list and remove all zero-sum sequences at once.
Algorithm
-
First Pass: Build Prefix Sum Map a. Create a
dummynode with value 0 and setdummy.next = head. b. Create aHashMap<Integer, ListNode>to store the last seen node for each prefix sum. c. InitializeprefixSum = 0and a pointercurrent = dummy. d. Iterate through the list fromdummyto the end. In each step, updateprefixSum += current.valand store the mapping in the hash map:map.put(prefixSum, current). If a prefix sum is encountered more than once, its entry in the map will be overwritten with the latest node. This ensures the map holds the final occurrence of each prefix sum. -
Second Pass: Rewire the Linked List a. Reset
prefixSum = 0andcurrent = dummy. b. Iterate through the list again fromdummyto the end. c. In each step, updateprefixSum += current.val. d. Use the map built in the first pass to find the correct next node. The sublist betweencurrentandmap.get(prefixSum)sums to zero. Therefore, we can skip this entire sublist by settingcurrent.next = map.get(prefixSum).next. e. Advancecurrentto its (potentially new) next node. -
Return
dummy.next.
Walkthrough
This method is both efficient and easier to reason about compared to the single-pass version.
First Pass: We traverse the entire linked list once to populate a hash map. The map stores key-value pairs of (prefixSum, node). When we calculate the prefix sum at each node, we place it in the map. If a prefix sum value already exists, we overwrite it. This is a key step: by the end of the pass, the map will only contain the last node at which each prefix sum occurred. This elegantly handles nested or overlapping zero-sum sequences, as it automatically finds the largest possible sequence to remove.
Second Pass: We traverse the list again, starting from the dummy node. For each node current, we calculate its prefix sum. We then look up this sum in our map. The node retrieved from the map, map.get(prefixSum), is the end of a potential zero-sum sequence that starts right after current. We can therefore perform the deletion by simply setting current.next to map.get(prefixSum).next. This single assignment correctly skips all intermediate nodes. We repeat this for every node in our second traversal, ensuring all necessary connections are made.
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */import java.util.HashMap;import java.util.Map; class Solution { public ListNode removeZeroSumSublists(ListNode head) { ListNode dummy = new ListNode(0, head); Map<Integer, ListNode> map = new HashMap<>(); int prefixSum = 0; // First pass: build the map of prefix sums to the last node seen with that sum ListNode current = dummy; while (current != null) { prefixSum += current.val; map.put(prefixSum, current); current = current.next; } // Second pass: rewire the list prefixSum = 0; current = dummy; while (current != null) { prefixSum += current.val; // Connect current node to the node after the last occurrence of its prefix sum current.next = map.get(prefixSum).next; current = current.next; } return dummy.next; }}Complexity
Time
O(N) - The algorithm consists of two sequential passes over the linked list. Each pass takes O(N) time. The total time complexity is O(N) + O(N) = O(N).
Space
O(N) - The hash map can store up to N+1 entries (for N nodes plus the dummy node) in the worst case where all prefix sums are unique.
Trade-offs
Pros
Optimal O(N) time complexity.
The logic is cleaner and more straightforward than the one-pass approach, as it separates the discovery phase (pass 1) from the modification phase (pass 2).
It is generally easier to implement correctly and debug.
Cons
Requires two full traversals of the linked list, which might have slightly more overhead than a one-pass solution.
Solutions
Solution
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode removeZeroSumSublists ( ListNode head ) { ListNode dummy = new ListNode ( 0 , head ); Map < Integer , ListNode > last = new HashMap <>(); int s = 0 ; ListNode cur = dummy ; while ( cur != null ) { s += cur . val ; last . put ( s , cur ); cur = cur . next ; } s = 0 ; cur = dummy ; while ( cur != null ) { s += cur . val ; cur . next = last . get ( s ). next ; cur = cur . next ; } return dummy . next ; } }/** * Definition for singly-linked list. * 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 , head ); unordered_map < int , ListNode *> last ; ListNode * cur = dummy ; int s = 0 ; while ( cur ) { s += cur -> val ; last [ s ] = cur ; cur = cur -> next ; } s = 0 ; cur = dummy ; while ( cur ) { s += cur -> val ; cur -> next = last [ s ] -> next ; cur = cur -> next ; } return dummy -> next ; } };# Definition for singly-linked list. # class ListNode: # def __init__(self, val=0, next=None): # self.val = val # self.next = next class Solution : def removeZeroSumSublists ( self , head : Optional [ ListNode ]) -> Optional [ ListNode ]: dummy = ListNode ( next = head ) last = {} s , cur = 0 , dummy while cur : s += cur . val last [ s ] = cur cur = cur . next s , cur = 0 , dummy while cur : s += cur . val cur . next = last [ s ]. next cur = cur . next return dummy . nextVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.