430. Flatten a Multilevel Doubly Linked List
Problem Description
You have a special doubly linked list where each node contains:
- A
next
pointer (pointing to the next node) - A
prev
pointer (pointing to the previous node) - A
child
pointer (which may point to another doubly linked list)
This creates a multilevel data structure where nodes can have "child" lists branching off them, and those child lists can have their own children, creating multiple levels.
Your task is to flatten this multilevel structure into a single-level doubly linked list.
The flattening rules are:
- When you encounter a node with a child list, the child list should be inserted right after the current node
- The child list nodes should appear after the current node but before the current node's original next node
- After flattening, all nodes should be in a single level with proper
prev
andnext
connections - All
child
pointers must be set tonull
after flattening
For example, if node A has next node B and child node C (which has its own list C->D->E), after flattening the order would be: A->C->D->E->B.
The function should return the head
of the flattened list. If the input head
is null
, return null
.
The solution uses a recursive approach that performs a preorder-like traversal. It processes each node by:
- Connecting it to the previous node with proper
prev
andnext
pointers - Saving the original
next
node - Recursively flattening the child list first
- Then continuing with the original
next
node - Setting the
child
pointer tonull
after processing
A dummy node is used to simplify edge cases when connecting the head of the list.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: While this problem involves a linked list structure, the multilevel nature with child pointers creates a graph-like structure. Each node can be viewed as a vertex, and the
next
,prev
, andchild
pointers represent edges between nodes. The child pointers specifically create a tree-like branching structure.
Is it a tree?
- Yes: The multilevel doubly linked list with child pointers forms a tree structure. Each node can have at most one child branch, and there are no cycles (a node's child list doesn't loop back to any ancestor). The main list with child branches hanging off it creates a tree where:
- The main horizontal list forms the primary path
- Child pointers create subtrees branching downward
- Each subtree can have its own branches
DFS
- Yes: Since we identified this as a tree structure, the flowchart leads us to use DFS (Depth-First Search).
Conclusion: The flowchart suggests using DFS for this problem.
This makes perfect sense because:
- We need to traverse into child lists (going deep) before continuing with the next sibling
- When we encounter a node with a child, we must fully explore and flatten that child's subtree before moving to the original next node
- The recursive nature of DFS naturally handles the multilevel structure - we dive deep into child lists, flatten them completely, then backtrack to continue with the main list
- The solution's
preorder
function is essentially a DFS traversal that processes nodes in a depth-first manner, exploring child branches before siblings
Intuition
When we look at this multilevel linked list, we can think of it like a document with nested bullet points. The main list is like the top-level bullets, and child lists are like sub-bullets that need to be "promoted" to the same level.
The key insight is that when we encounter a node with a child, we need to:
- Remember where we were going (save the
next
node) - Explore the entire child branch first
- Insert all those child nodes into our main path
- Then continue where we left off
This naturally suggests a recursive DFS approach. Why? Because each time we see a child, we face the exact same subproblem - we need to flatten that child's list and connect it properly. The child might have its own children, creating a recursive structure.
Think of it like exploring a maze where some rooms have trapdoors to lower levels. When you find a trapdoor (child pointer), you:
- Mark your current position
- Go down and explore the entire lower level
- Come back up with all the rooms from below "flattened" into your current path
- Continue exploring from where you left off
The clever part of the solution is how it maintains connections while traversing. As we visit each node in DFS order, we immediately establish the prev
and next
connections with the previous node. This way, we're building the flattened list as we traverse, rather than collecting nodes first and then connecting them.
The preorder
function returns the tail of the processed segment, which is crucial. After processing a child branch, we need to know where that branch ends so we can connect it to the original next
node. This tail-tracking ensures we maintain proper connections throughout the recursive calls.
Using a dummy node at the start simplifies edge cases - we don't need special logic for connecting the very first node, as it's treated like any other node in the traversal.
Solution Approach
The solution implements a recursive DFS traversal with careful pointer manipulation. Let's break down the implementation step by step:
Core Function - preorder(pre, cur)
:
This recursive function takes two parameters:
pre
: the previous node that should connect tocur
cur
: the current node being processed
The function returns the tail (last node) of the flattened segment starting from cur
.
Key Steps in Each Recursive Call:
-
Base Case: If
cur
isNone
, returnpre
as there's nothing to process. -
Connect Current Node:
cur.prev = pre pre.next = cur
This establishes the doubly-linked connection between the previous and current nodes.
-
Save Original Next:
t = cur.next
Before diving into the child, we save the original
next
pointer because we'll need to reconnect to it later. -
Process Child Branch:
tail = preorder(cur, cur.child) cur.child = None
Recursively flatten the child branch if it exists. The function returns the tail of the flattened child segment. We also set
child
toNone
as required. -
Continue with Original Next:
return preorder(tail, t)
Connect the tail of the child segment (or current node if no child) to the original next node and continue processing.
Main Function Logic:
-
Handle empty list case by returning
None
-
Create a dummy node:
dummy = Node(0, None, head, None)
This simplifies the logic by treating the head like any other node.
-
Start the recursive flattening:
preorder(dummy, head)
-
Clean up and return:
dummy.next.prev = None return dummy.next
Remove the connection from head back to dummy and return the actual head.
Why This Works:
The algorithm processes nodes in the exact order they should appear in the final flattened list:
- Current node first
- Then all nodes in the child subtree (recursively flattened)
- Finally, the original next node and its subsequent nodes
The tail-tracking mechanism ensures that after processing each segment, we know exactly where to attach the next segment, maintaining proper doubly-linked connections throughout.
Time Complexity: O(n)
where n is the total number of nodes, as each node is visited exactly once.
Space Complexity: O(d)
where d is the maximum depth of the multilevel structure, due to the recursive call stack.
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 a small example to illustrate how the solution flattens a multilevel doubly linked list.
Initial Structure:
1---2---3---4 | 5---6 | 7---8
We want to flatten this into: 1->2->5->6->7->8->3->4
Step-by-step execution:
-
Start with dummy node
- Create dummy node pointing to head (1)
- Call
preorder(dummy, node_1)
-
Process node 1
- Connect:
dummy.next = 1
,1.prev = dummy
- Node 1 has no child
- Save
t = node_2
- Call
preorder(node_1, node_2)
- Connect:
-
Process node 2
- Connect:
1.next = 2
,2.prev = 1
- Node 2 has child (node 5)!
- Save
t = node_3
(original next) - Call
preorder(node_2, node_5)
to process child branch
- Connect:
-
Process node 5 (child branch)
- Connect:
2.next = 5
,5.prev = 2
- Node 5 has no child
- Save
t = node_6
- Call
preorder(node_5, node_6)
- Connect:
-
Process node 6
- Connect:
5.next = 6
,6.prev = 5
- Node 6 has child (node 7)!
- Save
t = None
(no next) - Call
preorder(node_6, node_7)
- Connect:
-
Process node 7 (nested child)
- Connect:
6.next = 7
,7.prev = 6
- Node 7 has no child
- Save
t = node_8
- Call
preorder(node_7, node_8)
- Connect:
-
Process node 8
- Connect:
7.next = 8
,8.prev = 7
- Node 8 has no child and no next
- Returns node 8 as tail
- Connect:
-
Backtrack and continue
- Back to step 6: tail of node 7's branch is node 8
- Back to step 5: tail of node 6's branch is node 8
- Back to step 4: tail of node 5's branch is node 8
- Back to step 3: tail of child branch is node 8
- Now connect tail (node 8) to saved next (node 3)
- Call
preorder(node_8, node_3)
-
Process node 3 (original path)
- Connect:
8.next = 3
,3.prev = 8
- Node 3 has no child
- Save
t = node_4
- Call
preorder(node_3, node_4)
- Connect:
-
Process node 4
- Connect:
3.next = 4
,4.prev = 3
- Node 4 has no child and no next
- Returns node 4 as final tail
- Connect:
Final Result:
dummy -> 1 <-> 2 <-> 5 <-> 6 <-> 7 <-> 8 <-> 3 <-> 4
After cleanup (removing dummy), we get:
1 <-> 2 <-> 5 <-> 6 <-> 7 <-> 8 <-> 3 <-> 4
All child pointers are set to None
, and the list is properly flattened with all nodes connected via prev
and next
pointers only.
Solution Implementation
1"""
2# Definition for a Node.
3class Node:
4 def __init__(self, val, prev, next, child):
5 self.val = val
6 self.prev = prev
7 self.next = next
8 self.child = child
9"""
10
11
12class Solution:
13 def flatten(self, head: 'Node') -> 'Node':
14 """
15 Flatten a multilevel doubly linked list.
16
17 The list has additional child pointers that may point to separate doubly linked lists.
18 These child lists may have one or more children of their own.
19 The goal is to flatten the list so all nodes appear in a single-level, doubly linked list.
20
21 Args:
22 head: The head of the multilevel doubly linked list
23
24 Returns:
25 The head of the flattened doubly linked list
26 """
27
28 def flatten_dfs(prev_node: 'Node', curr_node: 'Node') -> 'Node':
29 """
30 Recursively flatten the list using depth-first search.
31
32 This function connects the current node to the previous node,
33 then processes any child list before continuing with the next node.
34
35 Args:
36 prev_node: The previous node in the flattened list
37 curr_node: The current node being processed
38
39 Returns:
40 The last node (tail) of the flattened portion
41 """
42 # Base case: if current node is None, return the previous node as tail
43 if curr_node is None:
44 return prev_node
45
46 # Connect current node with previous node
47 curr_node.prev = prev_node
48 prev_node.next = curr_node
49
50 # Save the original next node before we potentially overwrite it
51 temp_next = curr_node.next
52
53 # Recursively flatten the child branch first (if it exists)
54 # This will return the tail of the child branch
55 tail = flatten_dfs(curr_node, curr_node.child)
56
57 # Clear the child pointer as we've flattened this level
58 curr_node.child = None
59
60 # Continue flattening with the original next node
61 # Connect it after the tail of the child branch
62 return flatten_dfs(tail, temp_next)
63
64 # Handle empty list
65 if head is None:
66 return None
67
68 # Create a dummy node to simplify edge cases
69 # The dummy node acts as a predecessor to the actual head
70 dummy = Node(0, None, head, None)
71
72 # Start the flattening process
73 flatten_dfs(dummy, head)
74
75 # Disconnect the dummy node from the result
76 dummy.next.prev = None
77
78 # Return the actual head of the flattened list
79 return dummy.next
80
1/*
2// Definition for a Node.
3class Node {
4 public int val;
5 public Node prev;
6 public Node next;
7 public Node child;
8};
9*/
10
11class Solution {
12 /**
13 * Flattens a multilevel doubly linked list into a single level.
14 * @param head The head of the multilevel doubly linked list
15 * @return The head of the flattened list
16 */
17 public Node flatten(Node head) {
18 // Handle empty list
19 if (head == null) {
20 return null;
21 }
22
23 // Create dummy node to simplify edge cases
24 Node dummyHead = new Node();
25 dummyHead.next = head;
26
27 // Perform DFS traversal to flatten the list
28 flattenDFS(dummyHead, head);
29
30 // Disconnect the dummy head from the actual head
31 dummyHead.next.prev = null;
32
33 return dummyHead.next;
34 }
35
36 /**
37 * Recursively flattens the list using DFS traversal.
38 * Connects nodes in preorder fashion: current -> child subtree -> next subtree
39 * @param previous The previous node in the flattened list
40 * @param current The current node being processed
41 * @return The last node in the flattened portion
42 */
43 private Node flattenDFS(Node previous, Node current) {
44 // Base case: reached end of current branch
45 if (current == null) {
46 return previous;
47 }
48
49 // Connect current node to previous node
50 current.prev = previous;
51 previous.next = current;
52
53 // Store the original next node before it gets overwritten
54 Node tempNext = current.next;
55
56 // Recursively flatten the child branch first
57 Node tail = flattenDFS(current, current.child);
58
59 // Clear the child pointer after flattening
60 current.child = null;
61
62 // Continue flattening with the original next branch
63 return flattenDFS(tail, tempNext);
64 }
65}
66
1/*
2// Definition for a Node.
3class Node {
4public:
5 int val;
6 Node* prev;
7 Node* next;
8 Node* child;
9};
10*/
11
12class Solution {
13public:
14 /**
15 * Flattens a multilevel doubly linked list into a single level list
16 * @param head: The head of the multilevel doubly linked list
17 * @return: The head of the flattened list
18 */
19 Node* flatten(Node* head) {
20 flattenGetTail(head);
21 return head;
22 }
23
24private:
25 /**
26 * Helper function that flattens the list and returns the tail node
27 * @param head: The head of the current list segment
28 * @return: The tail node of the flattened list segment
29 */
30 Node* flattenGetTail(Node* head) {
31 Node* current = head;
32 Node* lastNode = nullptr;
33
34 // Traverse through the current level
35 while (current) {
36 // Store the next node before modifying pointers
37 Node* nextNode = current->next;
38
39 // If current node has a child, we need to flatten it
40 if (current->child) {
41 Node* childHead = current->child;
42
43 // Recursively flatten the child list and get its tail
44 Node* childTail = flattenGetTail(childHead);
45
46 // Clear the child pointer as per flattening requirement
47 current->child = nullptr;
48
49 // Connect current node to child list
50 current->next = childHead;
51 childHead->prev = current;
52
53 // Connect child list tail to the original next node
54 childTail->next = nextNode;
55 if (nextNode) {
56 nextNode->prev = childTail;
57 }
58
59 // Update the last node to be the child list's tail
60 lastNode = childTail;
61 } else {
62 // No child, so current node could be the last node
63 lastNode = current;
64 }
65
66 // Move to the next node in the original list
67 current = nextNode;
68 }
69
70 return lastNode;
71 }
72};
73
1// Definition for a Node
2class Node {
3 val: number;
4 prev: Node | null;
5 next: Node | null;
6 child: Node | null;
7
8 constructor(val?: number, prev?: Node | null, next?: Node | null, child?: Node | null) {
9 this.val = (val === undefined ? 0 : val);
10 this.prev = (prev === undefined ? null : prev);
11 this.next = (next === undefined ? null : next);
12 this.child = (child === undefined ? null : child);
13 }
14}
15
16/**
17 * Flattens a multilevel doubly linked list into a single level list
18 * @param head - The head of the multilevel doubly linked list
19 * @return The head of the flattened list
20 */
21function flatten(head: Node | null): Node | null {
22 flattenGetTail(head);
23 return head;
24}
25
26/**
27 * Helper function that flattens the list and returns the tail node
28 * @param head - The head of the current list segment
29 * @return The tail node of the flattened list segment
30 */
31function flattenGetTail(head: Node | null): Node | null {
32 let currentNode: Node | null = head;
33 let lastNode: Node | null = null;
34
35 // Traverse through the current level
36 while (currentNode) {
37 // Store the next node before modifying pointers
38 const nextNode: Node | null = currentNode.next;
39
40 // If current node has a child, we need to flatten it
41 if (currentNode.child) {
42 const childHead: Node = currentNode.child;
43
44 // Recursively flatten the child list and get its tail
45 const childTail: Node | null = flattenGetTail(childHead);
46
47 // Clear the child pointer as per flattening requirement
48 currentNode.child = null;
49
50 // Connect current node to child list
51 currentNode.next = childHead;
52 childHead.prev = currentNode;
53
54 // Connect child list tail to the original next node
55 if (childTail) {
56 childTail.next = nextNode;
57 if (nextNode) {
58 nextNode.prev = childTail;
59 }
60 // Update the last node to be the child list's tail
61 lastNode = childTail;
62 }
63 } else {
64 // No child, so current node could be the last node
65 lastNode = currentNode;
66 }
67
68 // Move to the next node in the original list
69 currentNode = nextNode;
70 }
71
72 return lastNode;
73}
74
Time and Space Complexity
Time Complexity: O(n)
where n
is the total number of nodes in the multilevel doubly linked list.
The algorithm visits each node exactly once through the recursive preorder
function. For each node, it performs constant time operations:
- Setting
cur.prev = pre
-O(1)
- Setting
pre.next = cur
-O(1)
- Storing
cur.next
in temporary variable -O(1)
- Setting
cur.child = None
-O(1)
- Making recursive calls that collectively cover all nodes
Since each node is processed exactly once with constant time operations, the total time complexity is O(n)
.
Space Complexity: O(d)
where d
is the maximum depth of the multilevel structure.
The space complexity comes from the recursive call stack. In the worst case, when we have a deeply nested structure (like a linked list where each node has a child forming a chain), the recursion depth can be at most d
. Each recursive call uses O(1)
space for local variables (t
variable and parameters), so the total space used by the call stack is O(d)
.
In the worst case where the list forms a single chain through child pointers, d = n
, making the space complexity O(n)
. In the best case where the list is completely flat with no child pointers, d = 1
, making the space complexity O(1)
.
Common Pitfalls
1. Forgetting to Save the Original Next Pointer
One of the most common mistakes is not saving the original next
pointer before processing the child branch. If you directly process the child without saving next
, you'll lose the reference to the rest of the original list.
Incorrect approach:
def flatten_dfs(prev_node, curr_node):
if curr_node is None:
return prev_node
curr_node.prev = prev_node
prev_node.next = curr_node
# ❌ Wrong: Processing child without saving next
tail = flatten_dfs(curr_node, curr_node.child)
curr_node.child = None
# curr_node.next is already lost/overwritten!
return flatten_dfs(tail, curr_node.next)
Why this fails: When we recursively process the child, the child nodes will overwrite curr_node.next
. By the time we try to access curr_node.next
, it no longer points to the original next node but to the first child node.
Solution: Always save the next pointer before processing children:
temp_next = curr_node.next # Save it first!
tail = flatten_dfs(curr_node, curr_node.child)
curr_node.child = None
return flatten_dfs(tail, temp_next) # Use the saved reference
2. Not Properly Handling the Dummy Node's Connections
Another common pitfall is forgetting to clean up the dummy node's connection to the actual head, which can create an invalid doubly-linked list where the head has a non-null prev
pointer.
Incorrect approach:
def flatten(self, head):
if head is None:
return None
dummy = Node(0, None, head, None)
flatten_dfs(dummy, head)
# ❌ Wrong: Forgetting to disconnect dummy from head
return head # head.prev still points to dummy!
Why this fails: The head of a doubly-linked list should have prev = None
. If we don't disconnect the dummy node, the head will have prev
pointing to the dummy node, violating the expected structure.
Solution: Always disconnect the dummy node:
dummy.next.prev = None # Disconnect dummy from the result
return dummy.next # Return the actual head
3. Forgetting to Set Child Pointers to None
The problem explicitly requires all child pointers to be null
after flattening. Forgetting this step leaves dangling child pointers that create an invalid structure.
Incorrect approach:
def flatten_dfs(prev_node, curr_node):
if curr_node is None:
return prev_node
curr_node.prev = prev_node
prev_node.next = curr_node
temp_next = curr_node.next
tail = flatten_dfs(curr_node, curr_node.child)
# ❌ Wrong: Forgetting to clear child pointer
# curr_node.child = None # Missing this line!
return flatten_dfs(tail, temp_next)
Why this fails: The flattened list should be a pure doubly-linked list with no child pointers. Leaving child pointers intact means the structure isn't truly flattened.
Solution: Always set child to None after processing:
tail = flatten_dfs(curr_node, curr_node.child) curr_node.child = None # Clear the child pointer
Is the following code DFS or BFS?
void search(Node root) { if (!root) return; visit(root); root.visited = true; for (Node node in root.adjacent) { if (!node.visited) { search(node); } } }
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
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
Want a Structured Path to Master System Design Too? Don’t Miss This!