Problem overview
Each Node has prev and next pointers and a child pointer, which may lead to another doubly linked list, which may have children of its own. The function flatten must merge all the levels into one doubly linked list, in place, and return the head. A child list goes right after its parent node, ahead of the parent's old successor.
For levels 1-2-3-4-5-6, 7-8-9-10 under 3 and 11-12 under 8, the result is [1, 2, 3, 7, 8, 11, 12, 9, 10, 4, 5, 6]. For 1-2 with child 3 under 1, it is [1, 3, 2]. An empty list stays empty.
This lesson's version must not be recursive, and must run in O(n) time.
Approach: splice the child in, then keep walking
A recursive solution flattens each child before splicing it. Here the order of work is reversed, and no recursion is needed. Walk the list with one pointer. When you reach a node with a child, splice the child list in as it is, even if it still has children of its own. Then keep walking. Because the child nodes are now part of the main list, the walker passes through them next, and when it reaches a child node that has its own child, it splices that one in too. Deeper levels are handled when their parent is reached.
Splicing needs the last node of the child's own level, so a small helper, getTail, walks the child list along next until the end. That helper is not recursive and doesn't look at grandchildren. The walker itself covers every node once, and each getTail call covers the nodes of one level once, so the total is linear.
Step by step
- Walk with a copy of
head, in avar node. The parameter itself must stay unchanged because it is the return value. - Loop while
nodeis notnull. - Check for a child. Nodes without one just move the loop on.
- Save
node.nextinnext, since the link is about to be overwritten. - Find the child level's tail with
getTail(node.child)and store it aschildTail. - Link
nodeand its child in both directions:node.next = node.child, then the child'sprevpoints atnode. This comes beforechildis cleared. - Clear
node.child. - Link
childTailto the savednext, and, ifnextexists, linknext.prevback tochildTail. - Move to
node.nextoutside theif. After a splice this is the first child node, so the walk continues inside the spliced part and flattens nested levels without recursion. - Return
head. After the loopnodeisnull, so it can't be the result. - Write
getTail(node): copy the node into a mutable variable and advance while it has anext. It stops on the last node, not on thenullafter it.
The solution in Kotlin
class Solution {
fun getTail(node: Node?): Node? {
var node = node
while (node!!.next != null) {
node = node.next
}
return node
}
fun flatten(head: Node?): Node? {
var node = head
while (node != null) {
if (node.child != null) {
val next = node.next
val 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
}
}
The Kotlin details worth knowing:
- Shadowing a parameter.
var node = nodeingetTaildeclares a new local variable with the same name as the parameter. The parameter is read-only, but the copy can be reassigned. Kotlin warns that the name is shadowed. node!!.nextin the condition. The!!asserts thatnodeisn'tnulland throws if it is. After it succeeds, Kotlin smart-casts the local variable to non-null for the body of the loop, sonode.nextneeds no!!. This is safe becausegetTailis only called with a child that exists.- Property versus local.
node.childis a mutable property, so Kotlin doesn't smart-cast it afternode.child != null. That's whynode.child!!.prevneeds!!, whilenext, a localval, is smart-cast without it. - Returning
head, notnode. The localnodeisnullat the end of the loop.
Complexity
Time: O(n). The main loop moves forward one node per iteration, and every node of the flattened list is visited once. getTail is called once per child list, and it walks only that list's own level, so each node is walked by getTail at most once: a node sits on exactly one level, and when it is reached as part of a child level it is walked once. Together that is at most about 2n steps.
Space: O(1). Only a few pointers are used, with no recursion and no stack.
Edge cases and common mistakes
- Empty list.
flatten(null)returnsnull. The loop never runs andgetTailis never called. - Child on the last node.
nextisnull, so the child's tail stays the end of the list and noprevis set. - Calling
getTailonnull. It would throw aNullPointerException, so only call it whenchild != null. - Returning
node. It isnullafter the loop. Returnhead. - Clearing
childtoo early. The next two lines readnode.child, so clear it only after the links are made. - Skipping the back links. The list looks right going forward but is wrong going backward when
previs not updated at both ends of the splice.
Testing
- The three-level example flattens to
[1, 2, 3, 7, 8, 11, 12, 9, 10, 4, 5, 6], and itsprevlinks read the same order in reverse. 1-2with child3under 1 gives[1, 3, 2].- A list with no children is returned as it was.
nullgivesnull.
After flattening the multilevel linked list it becomes: