Start learning free

LESSON 276 OF 338 · LINKED LISTS

Flatten a Multilevel Doubly Linked List in Go: 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 13
    Ctrl + Enter to check

    GO · LINKED LISTS

    Flatten Multilevel List in Go, explained

    Problem overview

    The function flatten receives head, the first node of a doubly linked list whose nodes are *Node values with Val, Prev, Next and Child fields. A node's Child may point to the head of a separate doubly linked list, and nodes in that list can have children of their own. The task is to flatten everything into one single-level doubly linked list, in place, and return its head.

    The order is depth first: a child list goes right after its parent and before the parent's old next node. In the first example the levels are 1-2-3-4-5-6, with node 3 having the child list 7-8-9-10, and node 8 having the child list 11-12. The flattened list is [1, 2, 3, 7, 8, 11, 12, 9, 10, 4, 5, 6]. For 1-2 where node 1 has the child 3, the answer is [1, 3, 2]. An empty list stays empty.

    The statement asks for a recursive algorithm that runs in O(n) time.

    Approach: flatten a level and report its tail

    Splicing a child list into its parent's level takes four pieces: the parent, the parent's old next node, the child list's head and the child list's last node. The child's head is simply node.Child. The old next node is saved before any link changes. The missing piece is the last node of the flattened child list, and that is where the recursion helps.

    Define a helper, flattenLevel(node), that flattens the list starting at node in place and returns its tail, the last node of the flattened result. To flatten a child list, call the same helper on node.Child. The call flattens that child list completely, including all of its own descendants, and returns the node that must be connected to the saved next.

    The helper walks its level node by node. When a node has a child, it splices the flattened child in and then keeps walking with node = node.Next. After the splice, node.Next is the child list, so the walk goes through the spliced nodes and then continues with the rest of the original level. The helper remembers the last node it visited, and that is the tail it returns.

    Take 1-2 with node 1 having the child 3. At node 1: save next = node 2, flatten the child (the tail is node 3), link 1 to 3, set 3's Prev to 1, clear 1's Child, link 3's Next to node 2, and set node 2's Prev to node 3. The walk then visits 3 and 2, and the result is 1-3-2.

    Step by step

    1. Call flattenLevel(head) from flatten. The helper rewires pointers in place, so its returned tail is not needed here.
    2. Return head. Flattening never moves the first node, so it is still the head.
    3. In flattenLevel, set tail := node and return tail at the end. For a nil argument the loop never runs and the helper returns nil, which handles an empty list.
    4. Loop while node != nil. Go writes while as a for with only a condition.
    5. Check for a child: if node.Child != nil. Nodes without one need no rewiring.
    6. Save node.Next in next before it's overwritten, otherwise the rest of the level is unreachable.
    7. Flatten the child recursively and keep the returned tail in childTail.
    8. Link the child list after the node: node.Next = node.Child.
    9. Repair the backward direction and clear the child. node.Child.Prev = node must come first, because node.Child = nil removes the pointer it reads.
    10. Attach the saved next after childTail: childTail.Next = next. This works even when next is nil.
    11. Fix next.Prev, but only if next != nil. Writing to a field of a nil pointer panics.
    12. Record the last visited node with tail = node, then advance with node = node.Next. After a splice, the walk continues through the child nodes, so tail ends on the true last node of the whole flattened level.

    The solution in Go

    func flatten(head *Node) *Node {
    	flattenLevel(head)
    	return head
    }
    func flattenLevel(node *Node) *Node {
    	tail := node
    	for node != nil {
    		if node.Child != nil {
    			next := node.Next
    			childTail := flattenLevel(node.Child)
    			node.Next = node.Child
    			node.Child.Prev = node
    			node.Child = nil
    			childTail.Next = next
    			if next != nil {
    				next.Prev = childTail
    			}
    		}
    		tail = node
    		node = node.Next
    	}
    	return tail
    }
    

    Go details worth knowing:

    • Ignoring a return value. A call such as flattenLevel(head) can be used as a statement and its result discarded. Go requires explicit use only for variables and imports, not for return values.
    • Recursion on structs. Node contains pointers to Node, which Go allows. Each recursive call has its own node, tail, next and childTail.
    • The parameter is reused as the cursor. The loop assigns to node, which is only the function's local copy of the pointer, so the caller's variable is unaffected. This is why tail was saved from the original value.
    • Nil safety. The two nil cases are an empty list (handled by the loop condition) and a last node on its level (handled by if next != nil). Assigning childTail.Next = next with a nil next is fine.

    Complexity

    Time: O(n) when nesting is shallow, O(n * d) in the worst case, where d is the deepest level of nesting. Splicing a child costs a constant number of assignments, but the walk of the parent's level then moves through the nodes that the recursive call has just flattened, so a node is visited once for its own level and once more for each ancestor level that walks over it. Those later visits only find that Child is already nil. In the worst case, a chain where every node is the only child of the previous one, the number of visits grows like n * (n + 1) / 2: 55 visits for 10 nodes, 5,050 for 100 and 500,500 for 1,000.

    Space: O(d). The only extra memory is the recursion stack, one frame for each level of children being flattened. For that same chain, d is n.

    Edge cases and common mistakes

    • Empty list. flatten(nil) returns nil.
    • No children at all. The list is only traversed and returned unchanged.
    • A child on the last node of a level. Here next is nil. Skipping the next != nil check panics with a nil pointer dereference.
    • Forgetting to clear Child. The list would look flat when read through Next but would still carry child links, so it isn't really single-level.
    • Not repairing Prev. The forward direction would be right while walking backwards from the end gives a wrong order, because child.Prev and next.Prev still hold their old values.
    • Losing the old next node. Assigning node.Next = node.Child before saving next makes the rest of the level unreachable.
    • Wrong tail. The tail of a flattened level isn't always the old last node of that level, because a child list may have been spliced in after it. The helper therefore tracks the last node actually visited.

    Testing

    • The three-level example gives [1, 2, 3, 7, 8, 11, 12, 9, 10, 4, 5, 6], with every node's Prev matching its predecessor and no Child left.
    • 1-2 where node 1 has the child 3 gives [1, 3, 2].
    • An empty list gives an empty list.
    Keep going with GoAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free