Flatten Multilevel Doubly Linked List — Stack-Based DFS Explained
Advertisement
Problem Statement
You are given a doubly linked list, which contains nodes that have a
nextpointer, aprevpointer, and an additionalchildpointer. Thischildpointer may or may not point to a separate doubly linked list, also with these three kinds of pointers. These child lists may have one or more children of their own, and so on, to produce a multilevel data structure. Given theheadof the first level of the list, flatten the list so that all the nodes appear in a single-level, doubly linked list. Letcurrbe a node with a child list. The nodes in the child list should appear aftercurrand beforecurr.nextin the flattened list. Return theheadof the flattened list.
Constraints:
- The number of nodes in the list is in the range
[0, 1000] 1 <= Node.val <= 10^5- The depth of the nesting is at most
1000
Example 1:
Input: head = [1,2,3,4,5,6,null,null,null,7,8,9,10,null,null,11,12]
Output: [1,2,3,7,8,11,12,9,10,4,5,6]Example 2:
Input: head = [1,2,null,3]
Output: [1,3,2]
Explanation: Node 1 has child 3. Flatten: 1->3->2.Why This Problem Matters
Flatten Multilevel Doubly Linked List (LeetCode 430) is a medium problem that Microsoft and Amazon use because it tests two things simultaneously: traversal of a recursive/nested data structure, and in-place pointer manipulation on a doubly linked list. Both are skills that appear constantly in systems programming.
The problem models a real data structure: hierarchical menus, nested outline documents, tree representations of nested lists. The child pointer is essentially an "expand this node into a sub-list" operation. Flattening it means performing a DFS and inlining each child list at the point of its parent node.
The explicit stack approach mirrors iterative DFS — a fundamental pattern for traversing hierarchical structures without recursion. When you encounter a node with a child, you push the next node onto the stack (to come back to it later), and then continue into the child list. This is exactly how an iterative DFS works with an explicit stack.
The problem also reinforces the importance of correctly maintaining prev pointers in a doubly linked list — an easy-to-miss requirement that turns a seemingly complete solution into an incorrect one.
The Core Insight
Traverse the list node by node. When you encounter a node with a child:
- Save
node.nextonto a stack (you'll resume from here later) - Connect
node.next = node.childandnode.child.prev = node - Clear
node.child = None - Continue traversal into the child sub-list
When the current chain reaches a node where curr.next is None and the stack is non-empty:
- Pop the saved next from the stack
- Connect
curr.next = poppedandpopped.prev = curr - Continue traversal
The stack effectively saves "where to resume" after each child list is exhausted.
Visual Dry Run
Input: 1 <-> 2 <-> 3 <-> 4 <-> 5 <-> 6, with 3.child = 7 <-> 8 <-> 9 <-> 10, and 8.child = 11 <-> 12
Start: stack = [], curr = 1
| curr | child? | Stack | Action |
|---|---|---|---|
| 1 | No | [] | advance: curr = 2 |
| 2 | No | [] | advance: curr = 3 |
| 3 | Yes (7) | [4] | push 4. curr.next = 7; 7.prev = 3; 3.child = None. curr = 7 |
| 7 | No | [4] | advance: curr = 8 |
| 8 | Yes (11) | [4, 9] | push 9. curr.next = 11; 11.prev = 8; 8.child = None. curr = 11 |
| 11 | No | [4, 9] | advance: curr = 12 |
| 12 | No, next=None | [4, 9] | Pop 9. 12.next = 9; 9.prev = 12. curr = 9 |
| 9 | No | [4] | advance: curr = 10 |
| 10 | No, next=None | [4] | Pop 4. 10.next = 4; 4.prev = 10. curr = 4 |
| 4 | No | [] | advance: curr = 5 |
| 5 | No | [] | advance: curr = 6 |
| 6 | No, next=None | [] | Stack empty, done |
Output: 1 <-> 2 <-> 3 <-> 7 <-> 8 <-> 11 <-> 12 <-> 9 <-> 10 <-> 4 <-> 5 <-> 6
Solution (Optimal)
Python — Iterative Stack
def flatten(head):
if not head:
return head
stack = []
curr = head
while curr:
if curr.child:
# Save the rest of the current level
if curr.next:
stack.append(curr.next)
# Insert child list after curr
curr.next = curr.child
curr.next.prev = curr
curr.child = None # clear the child pointer
# If current node has no next and stack has saved nodes
if not curr.next and stack:
nxt = stack.pop()
curr.next = nxt
nxt.prev = curr
curr = curr.next
return headTime complexity: O(n) — each node is visited exactly once.
Space complexity: O(d) where d is the maximum nesting depth (stack depth).
Python — Recursive DFS
def flatten(head):
def dfs(node):
"""Flatten from node onward, return the last node of the flattened list."""
curr = node
last = None
while curr:
if curr.child:
child_head = curr.child
child_last = dfs(child_head) # flatten child list, get its last node
nxt = curr.next
curr.next = child_head
child_head.prev = curr
child_last.next = nxt
if nxt:
nxt.prev = child_last
curr.child = None
last = child_last
if nxt is None:
break
curr = nxt
else:
last = curr
curr = curr.next
return last
if not head:
return head
dfs(head)
return headJavaScript — Iterative Stack
var flatten = function(head) {
if (!head) return head;
const stack = [];
let curr = head;
while (curr !== null) {
if (curr.child !== null) {
if (curr.next !== null) {
stack.push(curr.next);
}
curr.next = curr.child;
curr.next.prev = curr;
curr.child = null;
}
if (curr.next === null && stack.length > 0) {
const nxt = stack.pop();
curr.next = nxt;
nxt.prev = curr;
}
curr = curr.next;
}
return head;
};Complexity:
| Approach | Time | Space |
|---|---|---|
| Iterative stack | O(n) | O(d) depth |
| Recursive DFS | O(n) | O(d) call stack |
Common Mistakes
1. Forgetting to set prev pointers.
This is a doubly linked list — every new connection requires updating both next and prev. Missing prev updates produces incorrect output silently (LeetCode may not catch it if the grader only checks next pointers, but it's wrong).
2. Forgetting to clear curr.child = None.
After connecting the child to the current node's next, the original child pointer must be cleared. Leaving curr.child set means the flattened list still has child pointers pointing into the list — incorrect structure.
3. Pushing curr.next onto the stack even when curr.next is None.
Only push if curr.next is non-null. Pushing None would later pop and try to connect None as a next node — causing errors.
4. Popping from the stack when curr.next is None and the stack is non-empty, but forgetting to set prev.
When you reconnect a saved node: curr.next = popped; popped.prev = curr. Both assignments are required.
5. Infinite loop if child connects back to a parent node. The problem guarantees no cycles, but with cycles the traversal would loop forever. Assume the input is valid.
Interview Tips
-
Relate to iterative DFS: "I'll use an explicit stack — this is essentially an iterative DFS where the stack saves the 'resume point' after each child list."
-
Draw the multilevel list: For any example, draw the hierarchy (parent level and child levels) visually. This makes the traversal order obvious.
-
Emphasize
prevpointers: "Since it's a doubly linked list, every time I connectA -> B, I must also setB.prev = A." -
Walk through the stack contents: Show what's on the stack after each child encounter and after each pop. This demonstrates you understand the algorithm mechanically.
-
Mention the recursive alternative: "A recursive approach has DFS flatten each child list and return the tail, which you can then connect to the saved
next. Same complexity, different code style."
Follow-up Questions
Q: What if the nesting can be arbitrarily deep? The iterative stack approach handles any depth without stack overflow (the Python call stack has a limit, but the explicit list-based stack doesn't). For very deep nesting, iterative is safer than recursive.
Q: What's the time complexity? O(n) — each node is visited exactly once. Whether via the iterative approach or recursion, every node is processed exactly once.
Q: How does this relate to tree flattening (LC 114)? LC 114 (Flatten Binary Tree to Linked List) flattens a binary tree to a singly linked list in pre-order. Same DFS concept but different data structure. Both use the "visit node, recurse into child, reconnect at tail" pattern.
Q: What if you want to flatten bottom-up instead of top-down? Process from the deepest level upward. This is the recursive approach — it implicitly flattens deeper levels first and returns the tail for connection.
Q: Is there an O(1) space solution? No — some auxiliary space is needed. Either the recursion stack or an explicit stack uses O(d) space where d is the nesting depth.
Key Takeaways
- The iterative stack models DFS: when you encounter a child, push
curr.next(resume point) and descend into the child list. - When
curr.nextis None and the stack is non-empty, pop and reconnect — this resumes the parent-level traversal. - Always update both
nextandprevfor every new connection — it's a doubly linked list. - Always clear
curr.child = Noneafter inlining the child. - Time O(n), Space O(d) where d is nesting depth — each node is visited once.
- This is essentially iterative DFS — the same pattern applies to tree traversal, graph traversal, and any hierarchical data structure flattening.
Advertisement