Problem overview
mergeKLists(lists) receives an Array<ListNode?>: k singly linked lists, each sorted in ascending order, where a null entry stands for an empty list. It must return the head of one sorted list that contains every value from all of them, or null if there are no values at all.
For the lists 1 → 4 → 5, 1 → 3 → 4 and 2 → 6, the result is 1 → 1 → 2 → 3 → 4 → 4 → 5 → 6. For lists = [], and for a single empty list, the result is an empty list, null.
The lesson asks for O(n × k) time, where n is the total number of nodes and k is the number of lists. Apart from the nodes of the answer, it uses constant extra space. A faster version with a heap has its own lesson.
Approach: always take the smallest head
Each input list is sorted, so its smallest unused value is always the node at its front, its head. The smallest value among all unused values is therefore the smallest of the k heads. Repeating "pick the smallest head, append it to the answer, move that list forward" produces the values in order, until every list is exhausted.
Finding the smallest head is the simple part: look at all k heads and compare. That scan costs O(k), and it happens once for each of the n output nodes, which is where O(n × k) comes from.
Two supporting tricks keep the code short. A dummy node placed before the answer means the first appended node needs no special case: the answer is always dummy.next. A tail variable points at the last node appended, so each new node is attached with tail.next.
On [[1, 4, 5], [1, 3, 4], [2, 6]] the heads at the start are 1, 1 and 2. The first list's 1 is picked, because the comparison is strict and the first of equal heads wins. The heads become 4, 1, 2, so the second list's 1 is next, then 2, then 3. The heads are then 4, 4 and 6, and so on, ending with the lone 6 once the other two lists are empty. The output is 1, 1, 2, 3, 4, 4, 5, 6.
Step by step
- Create the dummy node.
val dummy = ListNode()is a placeholder whose value never matters. - Create the tail.
var tail = dummypoints at the end of the answer so far. It is avarbecause it moves with every appended node. - Loop while any list has nodes. The helper
anyNonNull(lists), written at the end, answers that. - Start with no winner.
var ind = -1will hold the index of the list with the smallest head. - Scan every list.
for (i in lists.indices)visits each position of the array. - Update the winner. A list competes only if it isn't
null. It wins if no winner is chosen yet, or if its head is strictly smaller than the current winner's. - Append a new node with the winner's value to the end of the answer.
- Advance the winning list by replacing its entry in the array with the next node.
- Move
tailonto the node just added. - Return
dummy.nextonce the loop ends: the head of the merged list, ornullif nothing was appended. - Write the helper.
anyNonNullloops over the array and returnstrueat the first entry that isn'tnull. - Finish the helper with
falseafter the loop, since every entry wasnull.
The solution in Kotlin
class Solution {
fun mergeKLists(lists: Array<ListNode?>): ListNode? {
val dummy = ListNode()
var tail = dummy
while (anyNonNull(lists)) {
var ind = -1
for (i in lists.indices) {
if (lists[i] != null && (ind == -1 || lists[i]!!.`val` < lists[ind]!!.`val`)) {
ind = i
}
}
tail.next = ListNode(lists[ind]!!.`val`)
lists[ind] = lists[ind]!!.next
tail = tail.next!!
}
return dummy.next
}
private fun anyNonNull(lists: Array<ListNode?>): Boolean {
for (l in lists) {
if (l != null) {
return true
}
}
return false
}
}
Kotlin details worth a look:
- Nullable types.
ListNode?means a node ornull. The array's elements are nullable, and so isnext. - Why
!!appears. The compiler doesn't smart-cast an array element after a null check. Even right afterif (lists[i] != null), writinglists[i].`val`is rejected withonly safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type 'ListNode?'. The!!says "I know this isn't null", and it would throw if that were wrong. Here it is safe, because the loop condition guarantees that some list is non-null, and the scan only picks non-null entries. - The backticks around
val. The node's field is namedval, which is a Kotlin keyword, so it has to be written in backticks every time it is used. - Short-circuit evaluation. In
ind == -1 || lists[ind]!!.`val` ..., the right side runs only whenindis not -1. Without that guard,lists[-1]would throw. tail = tail.next!!.nextwas just assigned, so it can't be null.tailkeeps the typeListNodefrom the line that created it.private funkeeps the helper out of the class's public surface.- The input array is used up. Step 8 overwrites entries of
lists, so after the call every entry isnull. The nodes themselves are not changed, because the answer is built from new nodes created with the same values.
Complexity
Time: O(n × k). The outer loop runs once per output node, n times. Each run scans the k entries to find the smallest head, and anyNonNull does at most another k checks. That is O(k) per node.
Space: O(1) extra. The only memory beyond the answer's own nodes is the dummy node and a few variables. The answer needs n new nodes, which the lesson doesn't count as extra.
Edge cases and common mistakes
- No lists at all. With
lists = [],anyNonNullisfalseimmediately, anddummy.nextis stillnull. - Only empty lists.
[null]behaves the same way, and so does[null, null]. - Some lists empty.
[null, 1 → 5, null, 3]merges to1 → 3 → 5. The scan skipsnullentries. - Equal values. Duplicates across lists are all kept. Whichever of equal heads is chosen, the output values are the same.
- Dropping the
ind == -1test. Comparing withlists[ind]before any winner exists would index-1. - Forgetting to advance the list. If step 8 is skipped, the same head is chosen forever and the loop never ends.
- Comparing the nodes instead of their values.
lists[i]!! < lists[ind]!!doesn't compile. Compare thevalfields.
Testing
[1 → 4 → 5, 1 → 3 → 4, 2 → 6]gives1 → 1 → 2 → 3 → 4 → 4 → 5 → 6.[]gives an empty list.[null]gives an empty list.[null, -3 → 7, null, 0]gives-3 → 0 → 7.[2 → 2, 2]gives2 → 2 → 2.
