Facebook Pixel

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:

  1. When you encounter a node with a child list, the child list should be inserted right after the current node
  2. The child list nodes should appear after the current node but before the current node's original next node
  3. After flattening, all nodes should be in a single level with proper prev and next connections
  4. All child pointers must be set to null 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:

  1. Connecting it to the previous node with proper prev and next pointers
  2. Saving the original next node
  3. Recursively flattening the child list first
  4. Then continuing with the original next node
  5. Setting the child pointer to null 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, and child 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:

  1. We need to traverse into child lists (going deep) before continuing with the next sibling
  2. 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
  3. 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
  4. The solution's preorder function is essentially a DFS traversal that processes nodes in a depth-first manner, exploring child branches before siblings
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Remember where we were going (save the next node)
  2. Explore the entire child branch first
  3. Insert all those child nodes into our main path
  4. 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 to cur
  • 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:

  1. Base Case: If cur is None, return pre as there's nothing to process.

  2. Connect Current Node:

    cur.prev = pre
    pre.next = cur

    This establishes the doubly-linked connection between the previous and current nodes.

  3. 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.

  4. 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 to None as required.

  5. 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:

  1. Handle empty list case by returning None

  2. Create a dummy node:

    dummy = Node(0, None, head, None)

    This simplifies the logic by treating the head like any other node.

  3. Start the recursive flattening:

    preorder(dummy, head)
  4. 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 Evaluator

Example 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:

  1. Start with dummy node

    • Create dummy node pointing to head (1)
    • Call preorder(dummy, node_1)
  2. 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)
  3. 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
  4. 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)
  5. 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)
  6. 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)
  7. Process node 8

    • Connect: 7.next = 8, 8.prev = 7
    • Node 8 has no child and no next
    • Returns node 8 as tail
  8. 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)
  9. 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)
  10. Process node 4

    • Connect: 3.next = 4, 4.prev = 3
    • Node 4 has no child and no next
    • Returns node 4 as final tail

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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More