Flatten a Linked List
Flatten a multilevel doubly linked list where nodes may have a child pointer branching to another sublist. Learn the stack-based DFS approach and the elegant iterative insert-child-inline method — plus the sorted vertical list variant.
Table of contents
- Before we start
- Picture this first (no code yet)
- The actual problem
- First, the slow way (so you feel the pain)
- The turning point
- The one idea to remember
- Approach 1: Stack (explicit DFS)
- Approach 2: Inline insert (elegant, no stack)
- Watch it happen, frame by frame
- Full three-level trace summary
- A different "flatten" variant: sorted lists merging vertically
- Common traps
- Check yourself
- Practice problems
Before we start
Flattening a linked list sounds simple until you realize a node can have a child pointer that branches into an entirely separate list — and each child can have its own children. This creates a tree-like structure masquerading as a list. By the end you will be able to:
- Explain why a stack is a natural fit for saving "deferred" work when you branch into a child.
- Write both the stack-based and the inline-insert approaches from memory.
- Trace a three-level multilevel list, step by step.
Picture this first (no code yet)
A real-life story
You are reading a printed document page by page. Occasionally a page has a footnote that says "see Appendix A, page 12 for details." You stop the main document, go read the appendix, finish the appendix, and then come back to the main document exactly where you left off.
That "come back to where you left off" is a stack. You push your current position onto a stack before diving into the appendix. When the appendix ends, you pop the stack to recover your saved position and continue. The child pointer in a multilevel linked list is the footnote; the stack is your bookmark.
The actual problem
Flatten a Multilevel Doubly Linked List (LC #430):
Each node has
val,prev,next, andchild. Thechildfield optionally points to a separate doubly linked sublist. Flatten the list so all nodes appear in a single-level doubly linked list, with nodes in depth-first (DFS) order.
Example:
Level 1: 1 ⟷ 2 ⟷ 3 ⟷ 4 ⟷ 5 ⟷ 6
|
Level 2: 7 ⟷ 8 ⟷ 9 ⟷ 10
|
Level 3: 11 ⟷ 12
Expected output (DFS order):
1 ⟷ 2 ⟷ 3 ⟷ 7 ⟷ 8 ⟷ 11 ⟷ 12 ⟷ 9 ⟷ 10 ⟷ 4 ⟷ 5 ⟷ 6
After visiting node 3, you dive into child 7 before continuing to 4.
First, the slow way (so you feel the pain)
Recursive DFS: when you hit a node with a child, recursively flatten the child list first, find the tail of the flattened child, then reconnect. This works conceptually but function-call overhead on deeply nested input can overflow the call stack. For 10,000 levels, the Python default recursion limit (~1000) throws a RecursionError. The iterative stack avoids this entirely.
The turning point
Pause & think
You are visiting a node. It has a child. You must follow the child before continuing to next. But you don't want to lose next — you need to come back to it after the child list ends.
Where should you save the next node before you dive into the child? And after you finish the child list, how do you retrieve the saved next?
Think about it as a last-in-first-out structure: the most recently saved "continuation" is the one you need next.
The one idea to remember
The entire pattern in one sentence
Walk node by node; whenever you see a child, push the current next onto a stack and continue with child — when you reach a dead end, pop the stack to get the saved continuation.
Approach 1: Stack (explicit DFS)
def flatten(head):
if not head:
return head
stack = []
cur = head
while cur:
if cur.child:
if cur.next:
stack.append(cur.next) # save the continuation
cur.next = cur.child # dive into child
cur.child.prev = cur # fix prev pointer
cur.child = None # clear child field
elif not cur.next and stack:
# dead end: no next, no child → pop saved continuation
saved = stack.pop()
cur.next = saved
saved.prev = cur # fix prev pointer
cur = cur.next
return head
Line-by-line narration:
stack = []— our bookmark shelf for deferred continuations.cur = head— start from the front.if cur.child:— found a branch. Must go down before going right.stack.append(cur.next)— only push if there is a next; avoids pushing None.cur.next = cur.child— redirect next to child list.cur.child.prev = cur— keep the doubly-linked invariant.cur.child = None— clean up: node no longer has a child.elif not cur.next and stack:— reached end of a sub-list; pop bookmark.cur.next = saved; saved.prev = cur— re-attach the saved continuation.cur = cur.next— advance one step.
Approach 2: Inline insert (elegant, no stack)
Instead of a stack, insert the entire child list inline between cur and cur.next:
def flatten(head):
cur = head
while cur:
if cur.child:
child_head = cur.child
child_tail = child_head
while child_tail.next: # walk to the tail of child list
child_tail = child_tail.next
# insert child list between cur and cur.next
child_tail.next = cur.next
if cur.next:
cur.next.prev = child_tail
cur.next = child_head
child_head.prev = cur
cur.child = None
cur = cur.next
return head
Trade-off: simpler to read, but finding child_tail is O(child length) at each step — potentially O(n²) for deeply nested equal-length chains. The stack approach is always O(n) total.
Watch it happen, frame by frame
Input (abbreviated to track the key moments):
1 ⟷ 2 ⟷ 3 ⟷ 4
|
7 ⟷ 8
Using the stack approach:
cur=1: no child, no stack. cur=2.
cur=2: no child, no stack. cur=3.
cur=3: child=7. Push (4) onto stack.
3.next=7, 7.prev=3, 3.child=None.
cur=4 ... wait, cur=cur.next=7 now.
Let me re-trace:
cur=3: has child(7).
stack.append(3.next=4) → stack=[4]
3.next = 7, 7.prev = 3, 3.child = None
cur = 3.next = 7
cur=7: no child.
cur.next=8 exists → don't pop. cur=8.
cur=8: no child.
cur.next = None AND stack=[4] → pop.
saved=4. 8.next=4, 4.prev=8.
cur = 8.next = 4.
cur=4: no child, no stack. cur=None. Exit.
Final: 1⟷2⟷3⟷7⟷8⟷4 ✅
Full three-level trace summary
For the original example with three levels:
Level 1: 1⟷2⟷3⟷4⟷5⟷6 (3 has child 7)
Level 2: 7⟷8⟷9⟷10 (8 has child 11)
Level 3: 11⟷12
Stack evolution:
- At node 3: push(4), follow child 7. Stack = [4].
- At node 8: push(9), follow child 11. Stack = [4, 9].
- At node 12: no next, pop(9). Continue from 9. Stack = [4].
- At node 10: no next, pop(4). Continue from 4. Stack = [].
- At node 6: no next, stack empty. Done.
Result: 1⟷2⟷3⟷7⟷8⟷11⟷12⟷9⟷10⟷4⟷5⟷6 ✅
A different "flatten" variant: sorted lists merging vertically
Some problems define a linked list where each node also has a down pointer pointing to a sorted vertical chain. Flatten by merging these sorted chains bottom-up.
def flatten_sorted(root):
if not root or not root.next:
return root
# recursively flatten from the right
root.next = flatten_sorted(root.next)
# merge current node's down-list with the flattened right
root = merge_sorted(root, root.next)
return root
def merge_sorted(a, b):
if not a: return b
if not b: return a
if a.val <= b.val:
a.down = merge_sorted(a.down, b)
return a
else:
b.down = merge_sorted(a, b.down)
return b
This variant appears in GFG-style interview questions ("Flatten a Linked List" where each node has a right and a down pointer and the down-chains are sorted). The merge is the same merge pattern from Chapter 4.4, applied to down pointers instead of next.
Common traps
Watch out for these
- Not fixing
prevpointers. LC #430 is a doubly linked list. Every time you rewire anextpointer you must also update theprevpointer of the target node. Skipping this causes validation failures. - Pushing None onto the stack. When
cur.nextis already None, pushing it wastes a stack entry and causes a badcur.next = Noneassignment later. Always checkif cur.next:before pushing. - Not clearing
cur.child = None. If you rewirecur.nexttocur.childbut don't clearcur.child, the node still reports a child, which can confuse iterators or graders that check the child field. - Mixing up the inline-insert complexity. The inline approach scans to the child tail at each step — on a chain of 1000 nodes each with a 1000-node child, that's O(10⁶) operations. The stack approach stays O(n) regardless of nesting depth.
Remember this forever
Flatten multilevel list — stack approach
stack, cur = [], head
while cur:
if cur.child:
if cur.next: stack.append(cur.next)
cur.next = cur.child
cur.child.prev = cur
cur.child = None
elif not cur.next and stack:
saved = stack.pop()
cur.next = saved
saved.prev = cur
cur = cur.next
return head
Trap: fix prev pointers; clear child; only push next when it's not None.
Check yourself
Why does the stack grow as we go deeper into children?
Every time we follow a child, we push the current next onto the stack — that next is the "continuation" we need to return to after the child list exhausts. If the child itself has children, we push again before following the grandchild. The stack records the deferred continuations in reverse order: the most recently pushed is the most immediately needed continuation, which is exactly what a stack (LIFO) provides.
In the inline-insert approach, why do we walk to `child_tail` before relinking?
We need to attach cur.next (the original continuation) to the end of the child list, not the beginning. If we only knew child_head, we would lose the ability to find the tail without an extra pass. Walking child_tail = child_head; while child_tail.next: child_tail = child_tail.next gives us the last node of the child list, which is where we splice in cur.next. Then the child list naturally flows into the continuation.
Practice problems
| Problem | Difficulty | What to notice | Link |
|---|---|---|---|
| Flatten a Multilevel Doubly Linked List | Medium | Stack saves deferred next; must fix prev | LC #430 |
| Flatten Binary Tree to Linked List | Medium | Pre-order DFS; right pointer as next; left = None | LC #114 |
| Flatten Nested List Iterator | Medium | Stack of iterators; pop and peek pattern | LC #341 |
| Flatten a Linked List (sorted down-chains) | Medium | Merge bottom-up using Chapter 4.4 merge | GFG |
Next up: Add Two Numbers as a Linked List — where each node stores one digit and you must carry correctly across different-length numbers.