Start learning free

LESSON 283 OF 348 · LINKED LISTS

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

Practice O(n) 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 iterative (non-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 has prev and next pointers plus a child pointer. A child, when it isn't null, is the head of another doubly linked list, and nodes of that list may have children of their own. The method flatten receives the head of the top level and must make one single-level doubly linked list, in place. This version of the lesson asks for an iterative solution, with no recursion, in O(n) time.

    A child list goes right after its parent and before the parent's old next. In the first example, node 3 has the child list 7, 8, 9, 10, and node 8 has the child list 11, 12. The flattened order is 1, 2, 3, 7, 8, 11, 12, 9, 10, 4, 5, 6. For [1, 2] where node 1 has the child 3, the result is 1, 3, 2. No node may keep a child pointer, and every prev must match the new order.

    Approach: splice as you walk

    A single loop walks the list from the head. When the current node has a child, splice the child list in right after it: the parent's next becomes the child, and the last node of the child list is linked to the parent's old next. Then simply keep walking. The next node in the walk is the first child node, so the loop passes through the spliced nodes. If one of those has a child, it is spliced when the loop reaches it.

    That is why no recursion is needed. A deeper child list doesn't have to be flattened before the splice. It is flattened later, when the walk arrives at its parent, and by that time the parent is already part of the main level.

    To reconnect the end of the child list, you need its last node. A helper, getTail, walks forward from the child's head until there is no next. It only walks the child's own level, which hasn't been spliced into yet, so each node is scanned by it at most once.

    Trace [1, 2] where node 1 has the child 3. At node 1, next is saved as node 2. The tail of the child list is node 3. Link 1 to 3 both ways, clear the child, link 3 to 2 both ways. The loop moves to node 3, then to node 2, then to null.

    Step by step

    1. Walk with a separate variable. Start node at head, so head stays available to return.
    2. Loop while node is truthy. An object is truthy and null is falsy, so an empty list never enters the loop.
    3. Check for a child. Only nodes with a child need rewiring.
    4. Save node.next in next. The next line overwrites it, so without the copy the rest of the level would be lost.
    5. Find the child list's last node with this.getTail(node.child) and keep it in childTail. The helper looks only at the child's own level.
    6. Link the node and its child both ways: node.next = node.child and node.child.prev = node. Both read node.child, so they run before it is cleared.
    7. Clear node.child by setting it to null.
    8. Link childTail to next: set childTail.next = next, and if next exists, set next.prev = childTail. If the parent ended its level, next is null and there is nothing to point back.
    9. Advance with node = node.next. After a splice this moves into the child nodes, so nested levels get flattened one by one.
    10. Return head. The first node never changes. Returning node would give null, because the loop ends when it becomes null.
    11. Write getTail: while the current node has a next, move to it, then return the node. Stop on the node whose next is null, not on null itself, because you want the last node.

    The solution in TypeScript

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

    The TypeScript details worth knowing:

    • The assertion !. getTail takes Node | null, and node!.next tells the compiler to treat node as non-null. It is only a promise. The compiled JavaScript has no trace of it, so passing null would throw a TypeError at run time. The call site only passes a child that exists.
    • Same shape as head. let node = head; gives node the type Node | null, inferred from head. Reassigning it never changes head.
    • Order of the assignments. node.child.prev = node must run before node.child = null. After the second line there is no child to update.
    • Methods. this.getTail(...) needs the this. prefix, as for any method of the class. The lesson supplies its own Node class with val, prev, next and child.

    Complexity

    Time: O(n). The main loop visits every node once. getTail walks a child level before it is spliced, and the loop later walks the same nodes as part of the main level. Each node is scanned by getTail at most once, because after a level is spliced, no later getTail call starts inside it. So the total work is a small multiple of n.

    Space: O(1). There is no recursion and no extra container, only a few local variables. That is the advantage of this version over the recursive one, whose stack grows with the nesting depth.

    Edge cases and common mistakes

    • An empty list. flatten(null) returns null, because the loop never runs.
    • No children. The loop walks to the end and returns the same list.
    • A child on the last node. 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 line next.prev = childTail would throw a TypeError.
    • Calling getTail on the parent. It would return the end of the parent's level, not the child's, and the splice would link the wrong nodes.
    • Forgetting to clear child. The order of values looks right, but each spliced node still points to its old list, so the structure is not flat.
    • Forgetting prev. For the first example, walking backwards from node 6 should give 6, 5, 4, 10, 9, 12, 11, 8, 7, 3, 2, 1.
    • Skipping the spliced nodes. The advance must read node.next after the splice. If the loop jumped to the saved next, nested children would stay unflattened.

    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