Problem overview
The method mergeKLists receives an Array[ListNode] named lists. Each element is the head of a linked list sorted in ascending order, and an element may be null when its list is empty. The method must merge all the lists into one sorted list and return its head.
For lists = [[1,4,5],[1,3,4],[2,6]] the merged list is [1,1,2,3,4,4,5,6]. For an empty array the answer is the empty list, and for [[]], an array holding one empty list, it is also empty. The statement asks for O(n log k) time and O(k) extra space, where n is the total number of nodes and k is the number of lists.
Approach: always take the smallest head
The smallest remaining value overall must be the head of one of the lists, because each list is sorted. So the merge repeats one step: take the smallest head, append it to the result, and replace it with its successor in its own list.
The cost depends on how you find the smallest head. Scanning all k heads each time costs O(k) per output node, which gives O(n * k) overall. A min-heap (a priority queue that gives back the smallest element first) does the same job in O(log k): it holds at most one node per list, so it never has more than k elements, and each of the n output nodes costs one removal and at most one insertion. That is where O(n log k) comes from.
Step by step
- Create the heap and name it
minNodes. It is ascala.collection.mutable.PriorityQueue[ListNode], written with its full package name because nothing is imported. Scala'sPriorityQueueis a max-heap: it gives back the largest element first. To get the smallest value first, the ordering passed in a second argument list isscala.math.Ordering.by[ListNode, Int](node => node.`val`).reverse.Ordering.bycompares nodes through theirvalfield, and.reverseflips the order. The field name needs backticks, sincevalis a Scala keyword. - Visit each head with
for (head <- lists). - Skip empty lists. If
headis notnull, callenqueue(head). - Create a sentinel,
val dummy = new ListNode(). The merged list is built after it. - Keep a tail,
var tail = dummy, always pointing at the last appended node. - Loop while
minNodes.nonEmpty. Each iteration costsO(log k). - Take the smallest node with
val minNode = minNodes.dequeue(). - Append a new node holding
minNode.`val`:tail.next = new ListNode(minNode.`val`). - Feed the heap. If
minNode.nextis notnull, enqueue it. Each list supplies one node at a time until it runs out. - Advance the tail:
tail = tail.next. - Return
dummy.nextas the last expression, the head of the merged list.
The solution in Scala
class Solution {
def mergeKLists(lists: Array[ListNode]): ListNode = {
val minNodes = scala.collection.mutable.PriorityQueue[ListNode]()(scala.math.Ordering.by[ListNode, Int](node => node.`val`).reverse)
for (head <- lists) {
if (head != null) {
minNodes.enqueue(head)
}
}
val dummy = new ListNode()
var tail = dummy
while (minNodes.nonEmpty) {
val minNode = minNodes.dequeue()
tail.next = new ListNode(minNode.`val`)
if (minNode.next != null) {
minNodes.enqueue(minNode.next)
}
tail = tail.next
}
dummy.next
}
}
The Scala details worth knowing:
- Two argument lists.
PriorityQueue[ListNode]()(ordering)has an empty first list for the initial elements, and a second list for the ordering, which is normally supplied implicitly. Here the ordering is given explicitly. - Max-heap by default.
PriorityQueue(3, 1, 2).dequeue()returns 3. PassingOrdering.Int.reversemakes it return 1. The same idea, applied to a field, isOrdering.by(...).reverse. - Why the ordering is needed. There is no default ordering for
ListNode, so the compiler needs one to build aPriorityQueue[ListNode]. for (head <- lists)loops over the elements of an array. It is not a counted loop and has no index.- Empty array.
fordoes nothing for an array with no elements, and the heap stays empty. - The input is not modified. The code builds new nodes with
new ListNode(minNode.`val`)instead of relinking the existing ones, so the input lists stay as they were. This costsnnew nodes, which the output needs anyway. - The last expression is the result.
dummy.nextneeds noreturn.
Complexity
Time: O(n log k). The heap holds at most k nodes. Every one of the n nodes is enqueued once and dequeued once, and each of these operations takes O(log k). The initial loop over the heads adds at most k insertions, which is no more than n when there are no empty lists.
Space: O(k) extra for the heap. The output list takes n nodes, which is the result rather than working memory.
Edge cases and common mistakes
- No lists.
lists = []leaves the heap empty, so the loop never runs anddummy.nextisnull. - Empty lists inside the array. A
nullelement must not be enqueued: comparing it would fail with aNullPointerException. The check in the first loop handles[[]]and arrays that mix empty and non-empty lists. - Forgetting
.reverse. The merged list would come out in descending order. - Forgetting to enqueue the successor. Only the heads would be merged, and the rest of each list would be lost.
- Duplicate values. Equal values (the two 1s and two 4s in the example) are fine. The queue gives back either one first, and the output is the same.
- Negative values. The ordering compares
Ints, so values such as -3 sort correctly.[[-3,-1],[],[-2]]gives[-3,-2,-1]. - Returning
dummy. That would add the placeholder node at the front.
Testing
[[1,4,5],[1,3,4],[2,6]]gives[1,1,2,3,4,4,5,6].[]gives the empty list.[[]]gives the empty list.[[-3,-1],[],[-2]]gives[-3,-2,-1].- A single list such as
[[1,2]]comes back as an equal list made of new nodes.
