Problem overview
Each Node in this doubly linked list has prev and next pointers, plus a child pointer that may lead to a separate doubly linked list. Those child lists can have children of their own, to any depth. Flatten(head) must turn the whole structure into one single-level doubly linked list and return its head.
The order is depth first: when a node has a child list, that whole list (with its own descendants) goes right after the node, before the node's old next. For the first example, the top level is 1 to 6, 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 with a child list 3 under node 1 becomes [1, 3, 2]. An empty list stays empty.
The note asks for a recursive algorithm that runs in O(n) time.
Approach: flatten the child first, then splice it in
Walk along one level. Most nodes have no child and need nothing. When a node does have one, three things must happen:
- Flatten the child list. This is the recursive part: a child list is the same problem as the original, just smaller.
- Insert the flattened child list between the node and the node's old
next. - Clear the node's
childpointer.
To reconnect the old next after the child list, you need the last node of the flattened child list, its tail. So the helper does double duty: it flattens a list in place and returns the tail of the result. The caller uses that tail to attach the rest of its own level.
There is one subtle point about the walk. After a splice, the node's next is the child, so the loop continues through the spliced nodes and then on to the old next. That walk is harmless, because the spliced nodes have no children left, but it means the loop's last visited node is the true tail of the whole flattened level, nested nodes included.
Step by step
- Write
Flattenin terms of a helper.FlattenLevel(node)flattens the list starting atnodein place and returns the tail.Flattencalls it and returnshead. The pointers are rewired but no node is replaced, so the head never changes. - Declare the helper with a
Nodeparameter and aNodereturn type. - Track the tail. A variable
tailstarts atnode. For an empty list, the loop never runs and the helper returnsnull, which is the correct tail of nothing. - Loop over the level while
node != null. - Look for a child. Only nodes with a child need work.
- Save
node.nextinnext. The next step overwrites that pointer, and without the saved copy the rest of the level would be unreachable. - Recurse into the child and store the returned tail as
childTail. - Link the node and its child in both directions:
node.nextpoints to the child and the child'sprevpoints back. Do this before clearing the child pointer, because both lines read it. - Clear
node.child. A flattened list must not keep child pointers. - Reconnect the saved part of the level.
childTail.nextbecomesnext. Ifnextexists, itsprevmust point back atchildTail. If the node was the last in its level,nextisnulland there is no back pointer to set. - Advance. After the
if, remember the current node astail, then movenodeforward. - Return
tail, the last node visited.
The solution in C#
class Solution {
public Node FlattenLevel(Node node) {
Node tail = node;
while (node != null) {
if (node.child != null) {
Node next = node.next;
Node childTail = FlattenLevel(node.child);
node.next = node.child;
node.child.prev = node;
node.child = null;
childTail.next = next;
if (next != null) {
next.prev = childTail;
}
}
tail = node;
node = node.next;
}
return tail;
}
public Node Flatten(Node head) {
FlattenLevel(head);
return head;
}
}
Some details worth a closer look:
- Nodes are references.
Nodeis a class, soNode next = node.nextcopies a reference, not a node. The savednextstill points at the original node afternode.nextis reassigned. - The helper's result is ignored in
Flatten. C# allows calling a method that returns a value and discarding it. Only the recursive call inside the helper needs the tail. - The parameter
nodeis a local variable. Moving it forward inside the loop doesn't change the caller's variable. Assigning to a parameter only changes the method's own copy of the reference. - Null checks are explicit.
node.child != nullis how C# asks whether a child exists.
Tracing the second example
Start with 1---2 and node 1 having the child 3. At node 1, the saved next is node 2. The recursive call on node 3 finds no child and returns 3 as the tail. Then node 1's next becomes node 3, node 3's prev becomes node 1, and node 1's child is cleared. The tail's next becomes node 2, and node 2's prev becomes node 3. The loop continues from node 1 to 3 to 2, and the result is 1---3---2.
Complexity
Time. Splicing one child list in is a fixed number of pointer changes, and every node is visited by the loop of its own level. The catch is the walk after a splice: the loop of each ancestor level also walks over the nodes spliced in below it. A node at nesting depth d is therefore visited up to d + 1 times, so the running time is O(n · d). When the nesting is shallow, that is close to O(n), which is the target in the problem's note. In the worst case, a single chain where every node's child holds the next node, d is about n and the work is quadratic. Running this code on such a chain of 100, 200 and 400 nodes makes 5,050, 20,100 and 80,200 loop visits.
The iterative version in the second lesson walks each level once to find its tail and once in its main loop, which keeps the work at O(n) for every shape.
Space: O(d) for the recursion stack, where d is how deep the children nest. In the worst case, a chain of children inside children, that is O(n). No extra data structures are used.
Edge cases and common mistakes
- Empty list.
Flatten(null)returnsnullbecause the loop is skipped. - Child on the last node of a level. Here
nextisnull. Writingnext.prev = childTailwithout the check throws aNullReferenceException. - Forgetting to clear
child. The flattened list would still have child pointers, so it would not be a proper single-level list. - Losing the old
next. Ifnode.nextis overwritten before it is saved, the rest of the level disappears. - Skipping the
prevpointers. A list with correctnextlinks but wrongprevlinks looks right going forward and breaks going backward. - Clearing the child too early. Setting
node.child = nullbeforenode.child.prev = nodethrows aNullReferenceException. - Returning something other than the tail from the helper. The caller needs the last node to reconnect its level.
Testing
- The three-level example from the problem gives
[1, 2, 3, 7, 8, 11, 12, 9, 10, 4, 5, 6]. 1---2with child3under node 1 gives[1, 3, 2].- A single node with no child stays
[1]. - An empty list returns
null.
Also walk backward from the last node using prev. For the first example you should see 6, 5, 4, 10, 9, 12, 11, 8, 7, 3, 2, 1.
After flattening the multilevel linked list it becomes: