Problem overview
The function mergeKLists receives an array lists of k linked lists, each sorted in ascending order. It must merge them into one sorted linked list and return its head. Some of the lists can be empty (null), and the array itself can be empty.
For [[1,4,5],[1,3,4],[2,6]] the merged list is [1,1,2,3,4,4,5,6]. For [] and for [[]] the answer is an empty list.
The statement asks for O(n log k) time and O(k) extra space, where n is the total number of nodes.
Approach: always take the smallest of the k fronts
Each list is sorted, so the smallest unmerged value overall is one of the k current front nodes. The simplest solution scans all k fronts to find the minimum, appends it and repeats. That costs O(k) per output node, so O(n * k) in total.
A min-heap does the same job faster. A heap is a container that always gives back its smallest element, and it needs O(log k) time for both insertion and removal. The heap holds at most one node per list: the current front. Take the smallest from the heap, append it to the output, and put that node's successor in the heap. The heap never holds more than k nodes, and each of the n output nodes costs one removal and at most one insertion, which gives O(n log k).
Step by step
- Create the heap as a
java.util.PriorityQueue<ListNode>calledminNodes, ordered by each node'sval. The comparatorcompareBy { it.`val` }sorts ascending, so the smallest value is on top. - Loop over the heads in
lists. - Add every non-null head to the heap. Empty lists contribute nothing.
- Create a dummy node,
val dummy = ListNode(). The merged list is built after it, so the first appended node needs no special case. - Keep a walker,
var tail = dummy, always pointing at the last node appended. - Loop while the heap isn't empty. Each iteration takes one node out of the heap.
- Poll the smallest node into
minNode. - Append a new node with
minNode.`val`astail.next. The output is built from new nodes, so the input lists are left unchanged. - Add the successor to the heap if
minNode.nextexists. A list keeps feeding the heap one node at a time until it runs out. - Advance the walker with
tail = tail.next!!. - Return
dummy.next, the head of the merged list. For no input nodes it isnull.
The solution in Kotlin
class Solution {
fun mergeKLists(lists: Array<ListNode?>): ListNode? {
val minNodes = java.util.PriorityQueue<ListNode>(compareBy { it.`val` })
for (head in lists) {
if (head != null) {
minNodes.add(head)
}
}
val dummy = ListNode()
var tail = dummy
while (minNodes.isNotEmpty()) {
val minNode = minNodes.poll()
tail.next = ListNode(minNode.`val`)
if (minNode.next != null) {
minNodes.add(minNode.next)
}
tail = tail.next!!
}
return dummy.next
}
}
The Kotlin details worth knowing:
- Using a Java class. Kotlin runs on the JVM and can use Java's standard library directly.
java.util.PriorityQueueis Java's binary heap. Written with its full name, it needs no import. compareBy. It builds aComparatorfrom a function that picks the sort key.compareBy { it.`val` }orders nodes by theirval, ascending. The type ofitis inferred from the queue's element type,ListNode.- Backticks for
val.valis a keyword, but the node's field has that name, so it is writtenit.`val`. - Ties. Nodes with equal values may come out of the heap in any order. Since only the values are copied into the output, the result is the same either way.
- Platform types. Java methods like
poll()return a type that Kotlin doesn't mark nullable, sominNodeis used without!!. In this loop the heap is known to be non-empty, sopoll()always returns a node. LikewiseminNodes.add(minNode.next)compiles even thoughnextis nullable, because the Java parameter type accepts it, and theifguarantees it isn'tnullhere. tail.next!!.nextis a nullable property, and the compiler doesn't track that the previous line assigned it, so!!asserts it is notnull.- The type
Array<ListNode?>. It's an array of nullable nodes, soheadin the loop may benulland the check is needed.
Complexity
Time: O(n log k). Building the heap takes at most k insertions of O(log k) each. After that, each of the n output nodes costs one poll and at most one insertion, each O(log k).
Space: O(k) extra. The heap never holds more than one node per list. The output list has n new nodes, but that is the result itself.
Edge cases and common mistakes
- No lists.
arrayOf()leaves the heap empty, the loop never runs and the function returnsnull. - Only empty lists.
[null]adds nothing to the heap, with the same result. - Some empty lists.
[null, [2], null, [-1, 9]]gives[-1, 2, 9]. Thenullcheck on each head keepsnullout of the heap. - Negative values and duplicates. The comparator handles both. Equal values simply appear next to each other in the output.
- Forgetting to add the successor. The output would contain only the first node of each list.
- Adding a
nullsuccessor. APriorityQueuethrows onnull, so the check onminNode.nextmatters. - Returning
dummy. That includes the placeholder node with value 0 at the front.
Testing
[[1,4,5],[1,3,4],[2,6]]gives[1,1,2,3,4,4,5,6].[]gives an empty list (null).[[]]gives an empty list.[[], [2], [], [-1, 9]]gives[-1, 2, 9].