LeetCode 430 - Flatten a Multilevel Doubly Linked List
The problem gives us a special type of doubly linked list where every node contains four fields: The next and prev pointers behave exactly like a normal doubly linked list. The extra child pointer introduces another linked list that branches downward from the current node.
Difficulty: 🟡 Medium
Topics: Linked List, Depth-First Search, Doubly-Linked List
Solution
Problem Understanding
The problem gives us a special type of doubly linked list where every node contains four fields:
val
prev
next
child
The next and prev pointers behave exactly like a normal doubly linked list. The extra child pointer introduces another linked list that branches downward from the current node. That child list can itself contain nodes with their own child pointers, creating a recursive multilevel structure.
The task is to transform this multilevel structure into a single flattened doubly linked list. The flattening order follows a depth-first traversal pattern. Whenever a node has a child list, that entire child list must be inserted immediately after the current node and before the node that originally came next.
For example, suppose node 3 has a child list starting at 7:
1 - 2 - 3 - 4
|
7 - 8
After flattening, the list becomes:
1 - 2 - 3 - 7 - 8 - 4
The important detail is that the original ordering must still be preserved. The child list is inserted in place, not appended at the end.
The output must remain a valid doubly linked list. That means:
- Every
nextpointer must be correct - Every
prevpointer must be correct - Every
childpointer must be set tonull
The constraints tell us that the total number of nodes is at most 1000. This is small enough that even recursive solutions are safe in practice, although iterative approaches are also completely reasonable. Since the structure is pointer-based rather than array-based, the main challenge is correctly rewiring pointers without losing parts of the list.
Several edge cases are important:
- The input may be empty
- A node may have no child
- Multiple levels of nesting may exist
- A child list may itself contain children
- The tail of a child list must reconnect correctly to the original
nextnode - Every
childpointer must eventually becomenull
A naive implementation can easily lose references to parts of the list if it overwrites pointers before saving them.
Approaches
Brute-Force Approach
A straightforward brute-force strategy is to repeatedly scan the list looking for nodes with child pointers. Whenever such a node is found, we splice the child list into the main list, then continue scanning until no child pointers remain.
The algorithm works because every iteration reduces the number of remaining child pointers. Eventually the structure becomes fully flattened.
However, this approach is inefficient because the list may be rescanned many times. In deeply nested structures, the same nodes are visited repeatedly. Pointer manipulation also becomes cumbersome because reconnecting tails requires additional traversal work.
Although the constraints are relatively small, this repeated scanning introduces unnecessary overhead.
Optimal Approach
The key insight is that flattening naturally follows a depth-first traversal.
When we encounter a node with a child:
- Save the original
nextnode - Flatten the child list recursively
- Insert the flattened child list immediately after the current node
- Connect the tail of the child list back to the saved
nextnode
This works because a child list must appear completely before the original next node in the flattened ordering.
A recursive depth-first search is especially clean here because each recursive call can return the tail of the flattened sublist. That tail is exactly what we need to reconnect the remaining nodes.
Instead of rescanning portions of the list repeatedly, every node is processed once.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n²) | O(1) | Repeatedly scans the list to locate and splice child lists |
| Optimal | O(n) | O(n) | DFS flattening processes each node once |
Algorithm Walkthrough
Optimal DFS Algorithm
- Start from the head of the list and traverse node by node.
We process the list in normal left-to-right order using the next pointer.
2. For each node, first save its original next pointer.
This step is critical because once we attach the child list, the original next reference would otherwise be lost.
3. If the current node does not have a child, simply continue forward.
No structural modification is needed in this case. 4. If the current node has a child list, recursively flatten that child list.
The recursive call returns the tail node of the flattened child portion. This is important because we later need to reconnect the original next node after the child chain ends.
5. Insert the child list between the current node and the original next.
The connections become:
current.next = child_head
child_head.prev = current
- Remove the child pointer.
The problem explicitly requires every child pointer to become null.
7. Connect the tail of the flattened child list back to the saved next node.
If the original next node exists:
child_tail.next = next_node
next_node.prev = child_tail
- Continue traversal from the saved
nextnode.
This ensures the entire structure is processed in depth-first order. 9. Return the tail node of the flattened segment.
Returning the tail allows parent recursive calls to reconnect lists efficiently without rescanning.
Why it works
The algorithm maintains an important invariant:
At every recursive return, the sublist starting from the current node is completely flattened and correctly connected.
Because each child list is fully flattened before reconnecting the original next node, the final ordering exactly matches the required depth-first traversal order. Every node is visited once, and every pointer is updated consistently.
Python Solution
"""
# Definition for a Node.
class Node:
def __init__(self, val, prev, next, child):
self.val = val
self.prev = prev
self.next = next
self.child = child
"""
from typing import Optional
class Solution:
def flatten(self, head: 'Optional[Node]') -> 'Optional[Node]':
def dfs(node: 'Optional[Node]') -> 'Optional[Node]':
current = node
last = node
while current:
next_node = current.next
if current.child:
child_head = current.child
child_tail = dfs(child_head)
current.next = child_head
child_head.prev = current
current.child = None
if next_node:
child_tail.next = next_node
next_node.prev = child_tail
last = child_tail
else:
last = current
current = next_node
return last
if not head:
return None
dfs(head)
return head
The implementation uses a helper DFS function that recursively flattens a sublist and returns its tail node.
The variable current traverses the current level of the list. Before modifying pointers, the code stores current.next inside next_node. This prevents losing access to the remaining portion of the list after inserting the child chain.
When a child exists, the algorithm recursively flattens that child sublist. The returned child_tail is then used to reconnect the original next node.
The child list is inserted immediately after the current node:
current.next = child_head
child_head.prev = current
The child pointer is then cleared:
current.child = None
Finally, if the original next node existed, the flattened child tail reconnects to it.
The helper returns the tail of the fully flattened section so higher recursive levels can reconnect correctly without additional traversal.
Go Solution
/**
* Definition for a Node.
* type Node struct {
* Val int
* Prev *Node
* Next *Node
* Child *Node
* }
*/
func flatten(root *Node) *Node {
if root == nil {
return nil
}
var dfs func(node *Node) *Node
dfs = func(node *Node) *Node {
current := node
last := node
for current != nil {
nextNode := current.Next
if current.Child != nil {
childHead := current.Child
childTail := dfs(childHead)
current.Next = childHead
childHead.Prev = current
current.Child = nil
if nextNode != nil {
childTail.Next = nextNode
nextNode.Prev = childTail
}
last = childTail
} else {
last = current
}
current = nextNode
}
return last
}
dfs(root)
return root
}
The Go implementation closely mirrors the Python version. The main difference is explicit pointer handling with nil checks instead of Python's None.
Go also requires defining the recursive DFS function using a variable declaration before assignment:
var dfs func(node *Node) *Node
Since the constraints are small, recursion depth is safe.
Worked Examples
Example 1
Input:
1 - 2 - 3 - 4 - 5 - 6
|
7 - 8 - 9 - 10
|
11 - 12
Step-by-step traversal
| Current Node | Action | Result |
|---|---|---|
| 1 | No child | Move to 2 |
| 2 | No child | Move to 3 |
| 3 | Has child 7 | Save next node 4 |
| 7 | No child | Move to 8 |
| 8 | Has child 11 | Save next node 9 |
| 11 | No child | Move to 12 |
| 12 | No child | Return tail 12 |
| 8 | Attach 11-12 between 8 and 9 | List partially flattened |
| 9 | No child | Move to 10 |
| 10 | No child | Return tail 10 |
| 3 | Attach 7-8-11-12-9-10 between 3 and 4 | Main list updated |
| 4 | No child | Move to 5 |
| 5 | No child | Move to 6 |
| 6 | No child | Done |
Final flattened list:
1 - 2 - 3 - 7 - 8 - 11 - 12 - 9 - 10 - 4 - 5 - 6
Example 2
Input:
1 - 2
|
3
Step-by-step traversal
| Current Node | Action | Result |
|---|---|---|
| 1 | No child | Move to 2 |
| 2 | Has child 3 | Save next as null |
| 3 | No child | Return tail 3 |
| 2 | Attach child after 2 | Flatten complete |
Final list:
1 - 2 - 3
The serialized output becomes:
[1,3,2]
because the flattening inserts child nodes immediately after the parent.
Example 3
Input:
[]
Since the head is None, the function immediately returns None.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Every node is visited exactly once |
| Space | O(n) | Recursive call stack in worst-case nesting |
The algorithm performs constant-time pointer updates per node, so total work scales linearly with the number of nodes.
The recursive DFS introduces stack usage proportional to the maximum nesting depth. In the worst case, where every node contains a child and no next node, recursion depth can become O(n).
Test Cases
# Helper Node class
class Node:
def __init__(self, val, prev=None, next=None, child=None):
self.val = val
self.prev = prev
self.next = next
self.child = child
def list_values(head):
result = []
while head:
result.append(head.val)
# verify child pointers removed
assert head.child is None
head = head.next
return result
# Example 1
n1 = Node(1)
n2 = Node(2)
n3 = Node(3)
n4 = Node(4)
n5 = Node(5)
n6 = Node(6)
n7 = Node(7)
n8 = Node(8)
n9 = Node(9)
n10 = Node(10)
n11 = Node(11)
n12 = Node(12)
n1.next = n2
n2.prev = n1
n2.next = n3
n3.prev = n2
n3.next = n4
n4.prev = n3
n4.next = n5
n5.prev = n4
n5.next = n6
n6.prev = n5
n3.child = n7
n7.next = n8
n8.prev = n7
n8.next = n9
n9.prev = n8
n9.next = n10
n10.prev = n9
n8.child = n11
n11.next = n12
n12.prev = n11
head = Solution().flatten(n1)
assert list_values(head) == [1,2,3,7,8,11,12,9,10,4,5,6] # nested child lists
# Example 2
a = Node(1)
b = Node(2)
c = Node(3)
a.next = b
b.prev = a
b.child = c
head = Solution().flatten(a)
assert list_values(head) == [1,2,3] # single child insertion
# Example 3
assert Solution().flatten(None) is None # empty list
# Single node
single = Node(1)
head = Solution().flatten(single)
assert list_values(head) == [1] # minimal input
# Deep nesting
x1 = Node(1)
x2 = Node(2)
x3 = Node(3)
x1.child = x2
x2.child = x3
head = Solution().flatten(x1)
assert list_values(head) == [1,2,3] # recursive depth handling
# Child at tail
p1 = Node(1)
p2 = Node(2)
p3 = Node(3)
p1.next = p2
p2.prev = p1
p2.child = p3
head = Solution().flatten(p1)
assert list_values(head) == [1,2,3] # reconnect when next is None
| Test | Why |
|---|---|
| Empty list | Validates null input handling |
| Single node | Ensures trivial structure works |
| One child list | Tests basic splicing logic |
| Nested child lists | Validates recursive flattening |
| Deep nesting | Tests recursive depth correctness |
| Child at tail | Ensures reconnection logic works when next is null |
Edge Cases
Empty input list
The input head may be None. This is the simplest edge case, but forgetting to handle it can immediately cause null pointer errors during traversal.
The implementation checks this at the beginning:
if not head:
return None
This guarantees safe behavior before any recursive processing starts.
Child list at the tail
A node near the end of the list may contain a child while its next pointer is already null.
For example:
1 - 2
|
3
In this case, after flattening the child list, there is no original next node to reconnect. Incorrect implementations may accidentally dereference a null pointer or create invalid links.
The solution safely checks:
if next_node:
before reconnecting pointers.
Multiple levels of nesting
A child node may itself contain another child list several levels deep.
For example:
1
|
2
|
3
A shallow flattening strategy that only handles one level would fail here.
The recursive DFS ensures that every child list is fully flattened before reconnecting to the parent chain. Because each recursive call completely processes its subtree, arbitrary nesting depth is handled correctly.