Problem overview
The function flatten receives head, the first node of a doubly linked list made of *Node values with Val, Prev, Next and Child fields. A node's Child can point to the head of another doubly linked list, whose nodes may have children too. The function must flatten all levels into one doubly linked list, in place, and return its head. A child list goes right after its parent and before the parent's old next node.
For the levels 1-2-3-4-5-6, 7-8-9-10 (the child of node 3) and 11-12 (the child of node 8), the result 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]. An empty list stays empty.
This version must be iterative, with no recursion, and run in O(n) time.
Approach: splice children in as the walk reaches them
A recursive solution flattens each child before splicing it. The iterative version needs no recursion because of one observation: splicing a child list into the current level does not need that child list to be flat yet. If the child list still has children of its own, they can be spliced later, when the walk reaches them.
So the code makes one walk with a single cursor, node. At each node:
- If the node has no child, move on to
node.Next. - If it has a child, put the whole child list between
nodeand its old next node, then move on tonode.Next, which is now the head of the child list.
Because the walk then goes through the child list's nodes, any child of those nodes is spliced in when the walk gets there. Deeper levels get flattened one after another, using the same loop. For the three-level example, the walk reaches node 3 and inserts 7-8-9-10 before 4. Later, at node 8, it inserts 11-12 between 8 and 9, and the list becomes the expected order.
The splice needs the last node of the child's own level, so it can be connected to the saved next node. A small helper, getTail, finds it by following Next to the end. It runs only on the child's own level, which hasn't been spliced yet, so it never walks into a deeper level.
Step by step
- Start a cursor,
node := head. The head itself must not move, because it is the return value. - Loop while
node != nil, so every node of the growing list is visited. - Check for a child:
if node.Child != nil. - Save
node.Nextinnext. The splice overwrites that link. - Find the child level's last node with
childTail := getTail(node.Child). - Link the child after the node:
node.Next = node.Child. - Point the child's
Prevat the node:node.Child.Prev = node. It comes before the next step, because that step erases the pointer this one reads. - Clear the child:
node.Child = nil. Flattened nodes keep no child links. - Reattach the rest of the level:
childTail.Next = next. Anilvalue is fine if the node was last on its level. - Repair
next.PrevtochildTail, guarded byif next != nil. - Advance:
node = node.Next, outside theif. After a splice this enters the child list, which is how nested levels get flattened. - Return
head. The first node never changes. Returningnodewould givenil, because the loop ends when it becomesnil. - Write
getTail: whilenode.Next != nil, setnode = node.Next; then returnnode. It stops on the last node, not on thenilafter it.
The solution in Go
func flatten(head *Node) *Node {
node := head
for node != nil {
if node.Child != nil {
next := node.Next
childTail := getTail(node.Child)
node.Next = node.Child
node.Child.Prev = node
node.Child = nil
childTail.Next = next
if next != nil {
next.Prev = childTail
}
}
node = node.Next
}
return head
}
func getTail(node *Node) *Node {
for node.Next != nil {
node = node.Next
}
return node
}
Go details worth knowing:
- Parameters are local copies. In
getTail,node = node.Nextmoves only the function's own pointer. The caller'snode.Childis unchanged, so the caller still has the head of the child list after the call. - No
nilcheck ingetTail. It is only called insideif node.Child != nil, so its argument is nevernil. Calling it withnilwould panic with a nil pointer dereference atnode.Next. - Declaration order doesn't matter.
flattencallsgetTail, which is defined below it. Go resolves package-level functions no matter where they appear. - Short-lived variables.
nextandchildTailare declared inside theifblock, so they exist only for that splice, and a fresh pair is created each time.
Complexity
Time: O(n). The main loop visits every node once, because each node is spliced in once and then walked over once. getTail walks the child's own level once, and a node belongs to exactly one level's tail search, since the search happens before deeper levels are merged in. So each node is touched a constant number of times.
Space: O(1). There is no recursion and no stack or queue, only a few pointers.
Compared with the recursive lesson, this version has the same O(n) bound without needing stack space proportional to the nesting depth.
Edge cases and common mistakes
- Empty list. With
head == nilthe loop doesn't run, and the function returnsnil. - A child on the last node. Then
nextisnil. SettingchildTail.Next = nilis fine, butnext.Prevwould panic, hence the guard. - Child lists with children. The walk continues into the spliced nodes, so a child's child is handled when the walk reaches its parent. Moving to the old next node instead would skip this.
- Clearing
Childtoo early. Ifnode.Child = nilcomes beforenode.Child.Prev = node, the second line dereferencesniland panics. - Not saving
next. Afternode.Next = node.Child, the old next node is no longer reachable. - Leaving stale
Prevpointers. The forward order can be right while the backward links still point at the old neighbours. - Returning
node. After the loop it isnil.
Testing
- The three-level example gives
[1, 2, 3, 7, 8, 11, 12, 9, 10, 4, 5, 6], with eachPrevcorrect and noChildleft. 1-2where node 1 has the child3gives[1, 3, 2].- An empty list gives an empty list.
After flattening the multilevel linked list it becomes: