Problem overview
Each Node in this doubly linked list has prev and next pointers and a third pointer, child, which may point to the head of another doubly linked list. Those child lists can have children of their own. The function flatten must turn the whole structure into a single-level doubly linked list, in place, and return the head.
The order is depth first: when a node has a child list, that whole list (with everything below it) goes right after the node, before the node's old next. For the example with levels 1-2-3-4-5-6, 7-8-9-10 hanging below 3 and 11-12 hanging below 8, the result is [1, 2, 3, 7, 8, 11, 12, 9, 10, 4, 5, 6]. For 1-2 with a child 3 under 1, it is [1, 3, 2]. An empty list stays empty.
This lesson's version must be recursive and run in O(n) time.
Approach: flatten each child first, then splice it in
Think about one level. Walk along it, and whenever a node has a child, the child list must be spliced between that node and its old successor. The child list may itself contain children, so the splice is only correct after the child list is fully flat. That is a recursive structure: flattening a level means flattening each child level first.
The splice needs the last node of the flattened child list, because that node must connect forward to the old next. So the recursive helper returns the tail of the level it flattened. Each pointer is rewired once, and for a list that isn't nested very deeply the work is proportional to the number of nodes.
Step by step
- Write
flattenaround a helper.flattencallsflattenLevel(head)and returnshead. Everything happens in place, so the first node never changes. - Define
flattenLevel(node)taking the first node of a level, and returning the tail of the flattened level. Parameters are read-only in Kotlin, so copynodeintovar cur(the walker) andvar tail(the last node visited). For anulllevel the loop never runs andnullis returned. - Loop while
curis notnull. - Check for a child. Only nodes with a child need rewiring.
- Save
cur.nextinnextbefore changing it. Oncecur.nextpoints at the child, the rest of the level could not be reached any more. - Flatten the child level recursively and keep the tail it returns as
childTail. - Link
curto its child in both directions:cur.next = cur.child, and the child'sprevpoints back atcur. Both lines readcur.child, so they come before it is cleared. - Clear
cur.child. A flat list has no child pointers. - Link
childTailto the savednext: setchildTail.next, and, ifnextexists, setnext.prevback tochildTail. - Advance. After the
if, recordtail = curand move tocur.next. After a splice,cur.nextis the child, so the loop also walks the spliced nodes, andtailends on the real last node. - Return
tailafter the loop.
The solution in Kotlin
class Solution {
fun flattenLevel(node: Node?): Node? {
var cur = node
var tail = node
while (cur != null) {
if (cur.child != null) {
val next = cur.next
val childTail = flattenLevel(cur.child)
cur.next = cur.child
cur.child!!.prev = cur
cur.child = null
childTail!!.next = next
if (next != null) {
next.prev = childTail
}
}
tail = cur
cur = cur.next
}
return tail
}
fun flatten(head: Node?): Node? {
flattenLevel(head)
return head
}
}
The Kotlin details worth knowing:
- Smart cast on locals only. Inside
while (cur != null), the localvar curis known to be non-null, socur.childandcur.nextneed no?.. Butcur.childis a mutable property, and another piece of code could change it between the check and the use, so Kotlin doesn't smart-cast it. That's whycur.child!!.prevneeds!!. childTail!!. The helper's return type isNode?, but for a non-empty child list the tail is nevernull, so!!is safe.- A local
valsmart-casts. The checkif (next != null)letsnext.prevcompile without!!, becausenextis a localvalthat can't change after the check. - The return value of
flattenLevelis ignored byflatten. Only the recursive call inside the helper uses it. Kotlin allows discarding a returned value.
Complexity
Time: about O(n) for shallow nesting. Each splice is a constant number of pointer changes, and each node's own level loop visits it once. But the spliced nodes are walked again by the loop of the level above, and again by the level above that. A node nested d levels deep is therefore visited up to d + 1 times, so the worst case is O(n * d), which grows to O(n²) when each level holds one node and the nesting is as deep as the list is long. For ordinary inputs, with few levels, it behaves like O(n).
Space: O(d), where d is the nesting depth, for the recursion stack. A very deep structure can overflow the stack.
Edge cases and common mistakes
- Empty list. With
head = nullthe loop doesn't run, andflattenreturnsnull. - Child on the last node. Then
nextisnull. The child's tail ends the list, and there's noprevto set. - Forgetting to clear
child. The list would be linked correctly but still show child pointers. - Losing the old
next. If you overwritecur.nextbefore saving it, the remainder of the level is lost. - Setting only
nextlinks. The result must also be correct backwards. Missingprevupdates give a list that looks right going forward but is broken in reverse. - Returning the wrong thing.
flattenreturns the originalhead. The tail is only for the recursion.
Testing
- The three-level example flattens to
[1, 2, 3, 7, 8, 11, 12, 9, 10, 4, 5, 6], and walking back from the last node throughprevgives6, 5, 4, 10, 9, 12, 11, 8, 7, 3, 2, 1. 1-2with child3under 1 gives[1, 3, 2].- A list with no children is returned unchanged.
nullgivesnull.
After flattening the multilevel linked list it becomes: