Problem overview
Each Node has prev and next pointers and a child pointer that may lead to another doubly linked list, which can have children of its own. Flatten(head) must rewire the nodes into one single-level doubly linked list and return the head. This version must not use recursion and must run in O(n) time.
A child list goes right after its parent node and before the parent's old next. For the first example, the top level 1 to 6 has a child list 7, 8, 9, 10 under node 3, 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]. The second example, 1---2 with child 3 under node 1, becomes [1, 3, 2], and an empty list stays empty.
Approach: splice as you walk
A recursive solution flattens each child before splicing it in. The iterative idea gets the same result with a single loop and a trick: splice the child list in without flattening it first, and keep walking.
When the walker reaches a node with a child, it moves the child list in right after the node. The walker then steps onto the first child node, and from there continues through the child list as if it had always been part of the level. If one of those nodes has a child of its own, the walker meets it later and splices that list in at that point. By the time the walker reaches the end, every child has been spliced in, at every depth, in the right order. A nested list is never lost, because its parent is always visited before the nodes that follow it.
To splice a child list in, you need its last node, so the old next can be attached after it. A small helper, GetTail, finds it by walking the child's own level. That walk only covers the child's own siblings. Deeper lists are handled later, when the main loop reaches their parents.
Step by step
- Start a walker.
nodebegins athead. The head itself must not move, because it is the value you return. - Loop while
node != null, so every node on the level, and later every spliced node, is visited. - Check for a child. Nodes without one just let the loop move on.
- Save
node.nextinnext. Pointingnode.nextat the child in a moment would otherwise make the rest of the level unreachable. - Find the child list's tail with
GetTail(node.child)and keep it aschildTail. It only walks the child's own level. - Link the node and its child both ways:
node.nextis the child and the child'sprevis the node. Both lines readnode.child, so they come before it is cleared. - Clear
node.child, because a flattened list has no child pointers. - Reconnect the saved part.
childTail.nextbecomesnext, and ifnextis notnull, itsprevpoints back tochildTail. - Advance
nodetonode.next. After a splice this is the first child node, so the loop walks through the spliced nodes and flattens deeper levels along the way. - Return
headafter the loop. Returningnodewould be wrong, because by then it isnull. - Write
GetTailaboveFlatten, taking and returning aNode. - Walk to the last node. Keep moving while
node.next != null, then returnnode. The loop must stop on the last real node, not on thenullafter it.
The solution in C#
class Solution {
public Node GetTail(Node node) {
while (node.next != null) {
node = node.next;
}
return node;
}
public Node Flatten(Node head) {
Node node = head;
while (node != null) {
if (node.child != null) {
Node next = node.next;
Node childTail = GetTail(node.child);
node.next = node.child;
node.child.prev = node;
node.child = null;
childTail.next = next;
if (next != null) {
next.prev = childTail;
}
}
node = node.next;
}
return head;
}
};
Some details worth a closer look:
- References, not copies.
Nodeis a class, soNode next = node.nextstores a reference to the same node. It keeps pointing at the old successor afternode.nextis reassigned. - Moving
nodedoesn't movehead. Both are separate variables that happen to start with the same reference. That is whyheadis still the first node when the loop ends. GetTailassumes a non-null argument. It readsnode.nextimmediately, which is safe because it's only called whennode.child != null.- The trailing semicolon after the class. C# permits a
;after a class declaration, so};compiles. It does nothing.
Tracing the first example
The walker visits 1 and 2, which have no children. At node 3 the child list 7, 8, 9, 10 is spliced in: the tail is 10, and 10 is attached to the old next, node 4. The list is now 1, 2, 3, 7, 8, 9, 10, 4, 5, 6, with node 8 still holding its child. The walker moves to 7, then reaches 8. Its child list 11, 12 is spliced between 8 and 9, giving 1, 2, 3, 7, 8, 11, 12, 9, 10, 4, 5, 6. The walker carries on through 11, 12, 9, 10, 4, 5 and 6 and stops. That is the expected order.
Complexity
Time: O(n). The main loop visits every node once, because a spliced node is only reached after it has been spliced in. GetTail walks each child list's own level exactly once, since that level is still untouched when its parent is reached. Together that is about two visits per node, and the splice itself is a fixed number of pointer changes.
Space: O(1). The code uses a few local references and no recursion or other data structures. That is the advantage over the recursive version, whose stack grows with the nesting depth.
Edge cases and common mistakes
- Empty list. With
head == nullthe loop is skipped andnullis returned. - A child on the last node of a level. Then
nextisnull, so the back link has to be guarded. Without the check,next.prev = childTailthrows aNullReferenceException. - Children inside children. They need no special code. The walker steps into the spliced list and finds them there.
- Not clearing
child. The result would still contain child pointers. - Clearing
childtoo early.node.child = nullbeforenode.child.prev = nodethrows aNullReferenceException. - Forgetting the
prevlinks. The list would look correct going forward and be wrong going backward. - Stopping
GetTailone step too late. A loop onnode != nullends withnodeequal tonull, and the next line would throw.
Testing
- The three-level example gives
[1, 2, 3, 7, 8, 11, 12, 9, 10, 4, 5, 6]. 1---2with child3under node 1 gives[1, 3, 2].- A list with no children comes back unchanged.
- An empty list gives
null.
To check the prev links, walk backward from the last node. For the first example, the order should be 6, 5, 4, 10, 9, 12, 11, 8, 7, 3, 2, 1.
After flattening the multilevel linked list it becomes: