Merge Nodes in Between Zeros
MedPrompt
You are given the head of a linked list, which contains a series of integers separated by 0's. The beginning and end of the linked list will have Node.val == 0.
For every two consecutive 0's, merge all the nodes lying in between them into a single node whose value is the sum of all the merged nodes. The modified list should not contain any 0's.
Return the head of the modified linked list.
Example 1:
Input: head = [0,3,1,0,4,5,2,0]
Output: [4,11]
Explanation:
The above figure represents the given linked list. The modified list contains
- The sum of the nodes marked in green: 3 + 1 = 4.
- The sum of the nodes marked in red: 4 + 5 + 2 = 11.Example 2:
Input: head = [0,1,0,3,0,2,2,0]
Output: [1,3,4]
Explanation:
The above figure represents the given linked list. The modified list contains
- The sum of the nodes marked in green: 1 = 1.
- The sum of the nodes marked in red: 3 = 3.
- The sum of the nodes marked in yellow: 2 + 2 = 4.
Constraints:
- The number of nodes in the list is in the range
[3, 2 * 105]. 0 <= Node.val <= 1000- There are no two consecutive nodes with
Node.val == 0. - The beginning and end of the linked list have
Node.val == 0.
Approaches
3 approaches with complexity analysis and trade-offs.
This approach involves iterating through the original linked list and building a new linked list to store the merged nodes. It's straightforward and doesn't alter the input list.
Algorithm
- Initialize a
dummynode and anewTailpointer to build the result list. - Initialize
currentSum = 0. - Iterate through the input list starting from
head.next. - If the current node's value is not 0, add it to
currentSum. - If the current node's value is 0, create a new node with
currentSum, append it to the result list usingnewTail, and resetcurrentSumto 0. - After the loop, return
dummy.next.
Walkthrough
We'll traverse the original list, keeping track of the sum of values in the current segment (between two zeros).
A dummy node is used to simplify the construction of the new list. A tail pointer will track the end of the new list.
We start iterating from the node after the initial zero.
For each node, if its value is not zero, we add it to a running sum.
When we encounter a zero, it signifies the end of a segment. We create a new node with the accumulated sum, append it to our new list, and reset the sum to zero for the next segment.
This process continues until we've traversed the entire original list. The final result is the list starting from the dummy node's next 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 mergeNodes(ListNode head) { ListNode dummy = new ListNode(0); ListNode newTail = dummy; ListNode current = head.next; // Skip the initial 0 int currentSum = 0; while (current != null) { if (current.val == 0) { // End of a segment if (currentSum > 0) { newTail.next = new ListNode(currentSum); newTail = newTail.next; } currentSum = 0; // Reset for the next segment } else { // Accumulate sum currentSum += current.val; } current = current.next; } return dummy.next; }}Complexity
Time
O(N), where N is the number of nodes in the list. We perform a single pass through the list.
Space
O(M), where M is the number of segments between zeros. This is because we create a new node for each segment. In the worst case, M can be proportional to N, leading to O(N) space complexity.
Trade-offs
Pros
Simple to understand and implement.
Does not modify the original input list.
Cons
Requires extra space to store the new list, which can be significant for large inputs.
This approach uses recursion to solve the problem by breaking it down into smaller subproblems. Each recursive call handles one segment (the nodes between two zeros), sums them up, and links to the result of the next segment.
Algorithm
- Define a recursive function
solve(node)that processes the list starting from a0-node. - Base Case: If
node.nextisnull, returnnull. - Move to the start of the segment:
startNode = node.next. - Iterate from
startNodeto find the next0, calculating the sum of values. - Update
startNode.valwith the calculated sum. - Recursively call
solveon the0-node that ends the current segment:startNode.next = solve(zeroNode). - Return
startNode. - The initial call is
solve(head).
Walkthrough
The main idea is to define a recursive function, say solve(node), that takes a 0-node as input and returns the head of the merged list for the part of the list that follows.
The base case for the recursion is when the node after the input 0-node is null, which means we've reached the end of the list. In this case, we return null.
In the recursive step, we first move to the node after the input 0-node. This is the start of a new segment. We can reuse this node to store the sum.
We then iterate forward from this node to find the next 0, summing up the values along the way.
Once we find the next 0, we update the value of the segment's starting node with the calculated sum.
The next pointer of this updated node is then set to the result of a recursive call on the 0-node we just found.
The initial call from the main function will be solve(head).
/** * 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 mergeNodes(ListNode head) { // The first node is a 0, so we start processing from it. // The recursive function will handle the segments. return solve(head); } private ListNode solve(ListNode node) { // 'node' is the 0 before the segment we need to process. node = node.next; // Move to the first node of the segment. // Base case: if we are at the end of the list (after the last 0). if (node == null) { return null; } // Find the sum of the current segment. ListNode runner = node; int sum = 0; while (runner.val != 0) { sum += runner.val; runner = runner.next; } // 'runner' is now at the next 0. // The start of the segment 'node' will be the new merged node. node.val = sum; // The next merged node is the result of the recursion on the rest of the list. node.next = solve(runner); return node; }}Complexity
Time
O(N), as each node is visited a constant number of times across all recursive calls.
Space
O(M), where M is the number of segments. This space is used by the recursion call stack. In the worst case, this can be O(N), which might lead to a `StackOverflowError` for very long lists.
Trade-offs
Pros
In-place modification of nodes saves heap memory.
Can be an elegant and concise solution.
Cons
Risk of
StackOverflowErrorfor deep recursion levels (long lists).Can be less intuitive than an iterative approach.
This is the most optimal approach. It modifies the linked list in-place using two pointers, avoiding both the extra space of a new list and the recursion stack overhead. This results in O(1) extra space complexity.
Algorithm
- Initialize
modify = headto act as the tail of the result list, which is built starting fromhead. - Initialize
fast = head.nextto traverse the list. - Loop while
fastis not null. - Inside the loop, start a nested loop to find the next
0and calculate thesumof the segment. - Once the segment sum is calculated, store it in
modify.next.val. - Advance
modifytomodify.next. - Advance
fastpast the0to the start of the next segment. - After the main loop, set
modify.next = nullto correctly terminate the list. - Return
head.next.
Walkthrough
We use two pointers. Let's call them modify and fast.
The modify pointer will act as the tail of our new, condensed list. It will always point to the last valid merged node. We initialize it to head.
The fast pointer will iterate through the original list to find segments and calculate their sums. We initialize it to head.next.
The algorithm works by finding a segment (from one 0 to the next), calculating its sum, and then placing that sum into the node right after the one modify is pointing to.
In each step of the main loop, fast moves forward, accumulating a sum until it hits a 0.
Once a 0 is found, we have the sum for the segment. We update the value of modify.next with this sum.
Then, we advance modify to modify.next, effectively extending our condensed list by one node.
The fast pointer is then advanced to the node after the 0 it just found, to start processing the next segment.
The loop continues until fast has traversed the entire list. Finally, we set modify.next = null to terminate the new list correctly. The result is head.next.
/** * 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 mergeNodes(ListNode head) { // The first node (head) is a 0 and acts as a sentinel. // 'modify' will be the tail of the new list. ListNode modify = head; // 'fast' will traverse the list. ListNode fast = head.next; while (fast != null) { int sum = 0; // 'fast' is at the beginning of a segment. // Accumulate sum until the next 0. while (fast.val != 0) { sum += fast.val; fast = fast.next; } // 'fast' is now at a 0 node. // The node after 'modify' will store the sum. modify.next.val = sum; // Move 'modify' to this node. modify = modify.next; // Move 'fast' to the start of the next segment. fast = fast.next; } // Terminate the modified list. modify.next = null; // The new list starts after the sentinel 'head'. return head.next; }}Complexity
Time
O(N), where N is the number of nodes. Although there are nested loops, each node is visited only a constant number of times by the `fast` pointer, resulting in a linear time complexity.
Space
O(1). The modification is done in-place, using only a few extra pointers. No significant extra space is required, regardless of the input size.
Trade-offs
Pros
Extremely space-efficient with O(1) complexity.
Avoids recursion, thus no risk of stack overflow.
Efficient in terms of both time and space.
Cons
Modifies the original list, which might not be desirable in all contexts.
The pointer manipulation can be slightly more complex to follow than the new-list approach.
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 mergeNodes ( ListNode head ) { ListNode dummy = new ListNode (); int s = 0 ; ListNode tail = dummy ; for ( ListNode cur = head . next ; cur != null ; cur = cur . next ) { if ( cur . val != 0 ) { s += cur . val ; } else { tail . next = new ListNode ( s ); tail = tail . next ; s = 0 ; } } 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 * mergeNodes ( ListNode * head ) { ListNode * dummy = new ListNode (); ListNode * tail = dummy ; int s = 0 ; for ( ListNode * cur = head -> next ; cur ; cur = cur -> next ) { if ( cur -> val ) s += cur -> val ; else { tail -> next = new ListNode ( s ); tail = tail -> next ; s = 0 ; } } 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 mergeNodes ( self , head : Optional [ ListNode ]) -> Optional [ ListNode ]: dummy = tail = ListNode () s = 0 cur = head . next while cur : if cur . val != 0 : s += cur . val else : tail . next = ListNode ( s ) tail = tail . next s = 0 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.