Start learning free

LESSON 282 OF 348 · LINKED LISTS

Flatten a Multilevel Doubly Linked List in TypeScript: Recursive O(n)

Practice O(n) Recursive Read the full explanation

You are given a doubly linked list which in addition to the next and previous pointers, it could have a child pointer, which may or may not point to a separate doubly linked list. These child lists may have one or more children of their own, and so on, to produce a multilevel data structure, as shown in the example below.

Flatten the list so that all the nodes appear in a single-level, doubly linked list. You are given the head of the first level of the list.

 

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]
Explanation:

The multilevel linked list in the input is as follows:



After flattening the multilevel linked list it becomes:


Example 2:

Input: head = [1,2,null,3]
Output: [1,3,2]
Explanation:

The input multilevel linked list is as follows:

  1---2---NULL
  |
  3---NULL

Example 3:

Input: head = []
Output: []

 

How multilevel linked list is represented in test case:

We use the multilevel linked list from Example 1 above:

 1---2---3---4---5---6--NULL
         |
         7---8---9---10--NULL
             |
             11--12--NULL

The serialization of each level is as follows:

[1,2,3,4,5,6,null]
[7,8,9,10,null]
[11,12,null]

To serialize all levels together we will add nulls in each level to signify no node connects to the upper node of the previous level. The serialization becomes:

[1,2,3,4,5,6,null]
[null,null,7,8,9,10,null]
[null,11,12,null]

Merging the serialization of each level and removing trailing nulls we obtain:

[1,2,3,4,5,6,null,null,null,7,8,9,10,null,null,11,12]

 


Note:

Your algorithm should be recursive and run in O(n) time.


STEP 1 OF 12
    Ctrl + Enter to check

    TYPESCRIPT · LINKED LISTS

    Flatten Multilevel List in TypeScript, explained

    Problem overview

    Each node of the list has prev and next pointers, like a normal doubly linked list, plus a child pointer. A child, when it isn't null, is the head of another doubly linked list, and nodes of that list can have children too. The method flatten receives the head of the top level and must turn the whole structure into one single-level doubly linked list. It returns the head, and the lesson asks for a recursive solution that runs in O(n) time.

    The rule is that a child list goes right after its parent and before the parent's old next. In the first example, the node 3 has a child list 7, 8, 9, 10, and node 8 has a child list 11, 12. The flattened order is 1, 2, 3, 7, 8, 11, 12, 9, 10, 4, 5, 6. In the second example, [1, 2] where 1 has the child 3 becomes 1, 3, 2.

    After flattening, no node may keep a child pointer, and every prev pointer must match the new order.

    Approach: flatten the child first, then splice it in

    Walk along a level. When a node has a child, three things have to happen: the child list gets flattened (it may contain children of its own), the flattened child list is inserted between the node and its old next, and the node's child is cleared.

    The recursion handles the first part. Call the same function on the child. It flattens that sub-list in place and tells you where the flattened sub-list ends. That last node is the tail, and it is what you need to reconnect the rest of the current level.

    So the helper flattenLevel does two jobs: it flattens in place, and it returns the last node of the flattened list. The public method flatten only needs the first job, because the head never changes.

    Take [1, 2] where node 1 has the child 3. At node 1, save next (node 2). Flatten the child, whose tail is node 3. Link 1 and 3 in both directions, clear the child, then link node 3 to node 2 in both directions. The list is now 1, 3, 2.

    Step by step

    1. Delegate to a helper. flatten calls this.flattenLevel(head) and returns head. Splicing only rewires pointers, so the first node stays first.
    2. Declare flattenLevel. It takes and returns Node | null. The parameter can be null for an empty list, and the walking variable ends as null.
    3. Track the last visited node in tail, starting with node. For an empty list the loop never runs and the function returns null.
    4. Loop while node is truthy to visit every node on the level.
    5. Check for a child. Only nodes with a child need rewiring.
    6. Save node.next in next. The next assignment overwrites node.next, and without this copy the rest of the level would be lost.
    7. Recurse on node.child and keep the result in childTail. The recursive call also flattens deeper levels.
    8. Link the node and its child in both directions: node.next = node.child and node.child.prev = node. Both lines read node.child, so they come before it is cleared.
    9. Clear node.child by setting it to null.
    10. Link childTail to next. Set childTail.next = next, and if next exists, set next.prev = childTail. When the parent was the last node of its level, next is null and there is no node to point back.
    11. Advance. Set tail = node, then node = node.next. After a splice, node.next is the first child node, so the loop walks through the spliced nodes too, and tail ends on the real last node.
    12. Return tail after the loop. This is the value the recursive call stores in childTail.

    The solution in TypeScript

    class Solution {
        flattenLevel(node: Node | null): Node | null {
            let tail = node;
            while (node) {
                if (node.child) {
                    let next = node.next;
                    let childTail = this.flattenLevel(node.child);
                    node.next = node.child;
                    node.child.prev = node;
                    node.child = null;
                    childTail!.next = next;
                    if (next) {
                        next.prev = childTail;
                    }
                }
                tail = node;
                node = node.next;
            }
            return tail;
        }
    
        flatten(head: Node | null): Node | null {
            this.flattenLevel(head);
            return head;
        }
    }
    

    The TypeScript details worth knowing:

    • Mutating a parameter. The loop reassigns node. That changes only the local variable. The caller's head still refers to the first node, which is why flatten can return it.
    • Order of the assignments. node.child.prev = node reads node.child, so it has to run before node.child = null. After that line, node.child is null and there is no node to give a prev.
    • The assertion childTail!. flattenLevel is typed to possibly return null, but for a non-null child it returns a real node. The ! tells the compiler to accept that. The compiled JavaScript has no trace of it, so no check happens at run time.
    • The name Node. The lesson supplies its own Node class with val, prev, next and child. Use that one.

    Complexity

    Time: O(n). The main loop walks each level node by node. After a splice it continues into the child nodes, which were already flattened, and they have no children left, so each one is visited by the loop only a constant number of times. Rewiring a node costs a fixed number of assignments.

    Space: O(d), where d is the deepest nesting of child lists. The only extra memory is the recursion stack, one frame per level. In the worst case, where every node is the only child of the previous one, d is n, but typical inputs are much shallower.

    Edge cases and common mistakes

    • An empty list. flatten(null) returns null, because the loop never runs.
    • No children. The list comes back unchanged.
    • A child on the last node of a level. For [1, 2] with a child list [3, 4] on node 2, next is null. The result is 1, 2, 3, 4, and without the if (next) check the code would fail by reading prev of null.
    • Forgetting to clear child. The values look right, but nodes still point at their old child lists, so the structure is not flat.
    • Forgetting prev pointers. A list that is correct going forward but not backward is not a valid doubly linked list. For the first example, walking back from node 6 should give 6, 5, 4, 10, 9, 12, 11, 8, 7, 3, 2, 1.
    • Losing the rest of the level. Overwriting node.next before saving it into next makes nodes 4, 5 and 6 unreachable.
    • Returning the wrong thing. flatten returns the original head. Returning what the helper returns would give the last node.

    Testing

    • The three-level list from the first example gives 1, 2, 3, 7, 8, 11, 12, 9, 10, 4, 5, 6.
    • [1, 2] with a child [3] on node 1 gives 1, 3, 2.
    • [1, 2] with a child [3, 4] on node 2 gives 1, 2, 3, 4.
    • An empty list gives an empty list.
    Keep going with TypeScriptAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free