Problem overview
Given an IntArray of positive integers nums and a number k, return the k smallest values, in any order. Duplicates count separately: with nums = [5, 9, 3, 6, 2, 1, 3, 2, 7, 5] and k = 4 the smallest values are 1, 2, 2 and 3, because the array holds two 2s and two 3s and only one of the 3s makes the cut.
The lesson asks for O(n log k) time and O(k) extra space. It says that faster solutions exist and are covered in later lessons.
Approach
The simplest solution sorts the whole array and takes the first k values. That costs O(n log n) time. It works, but it does more than the problem needs: it puts all n values in order when only the k smallest are wanted.
A heap does better by remembering only k values at a time. The idea:
- Keep the k smallest values seen so far in a max heap, a heap whose top is the greatest value it holds.
- That top is the weakest member of the group, the first one that should be thrown out.
- When a new value arrives and the heap is full, compare it with the top. If the new value is smaller, it belongs in the group and the top does not, so swap them. Otherwise ignore the new value.
The heap never holds more than k values, so each add or remove costs O(log k). With n values to process, the total is O(n log k), and the heap itself takes O(k) space.
Step by step
- Build a max heap. Kotlin on the JVM uses Java's
java.util.PriorityQueue, which is a min heap by default: the smallest value comes out first. Passing the comparatorcompareByDescending { it }reverses the order, so the greatest value is on top. The lambda picks the thing to compare, here the number itself. - Visit every value. A
forloop with the variablevaluegoes overnums. - Fill the heap first. While the heap has fewer than
kvalues (size < k), every value is accepted withadd. - Compare with the top. Once the heap is full,
peek()reads the greatest kept value without removing it. Only avaluestrictly smaller than that top deserves a place. - Swap. In that case,
poll()removes the current top andaddinserts the new value. The heap keeps exactlykvalues. - Empty the heap into an array. After the loop the heap holds the k smallest values. Create
resultas anIntArray(k)and fill positions0 until kwithpoll()calls. Because the top is always the greatest, the values come out from the largest to the smallest. The problem accepts any order, so that's fine.
The solution in Kotlin
class Solution {
fun kSmallest(nums: IntArray, k: Int): IntArray {
val smallestKNums = java.util.PriorityQueue<Int>(compareByDescending { it })
for (value in nums) {
if (smallestKNums.size < k) {
smallestKNums.add(value)
} else if (value < smallestKNums.peek()) {
smallestKNums.poll()
smallestKNums.add(value)
}
}
val result = IntArray(k)
for (i in 0 until k) {
result[i] = smallestKNums.poll()
}
return result
}
}
Kotlin details worth knowing:
- The class is written with its full name,
java.util.PriorityQueue, so no import is needed. You could writeimport java.util.PriorityQueueat the top of the file and use the short name instead. compareByDescending { it }returns aComparator. Kotlin works out the type ofitfromPriorityQueue<Int>, so the lambda needs no annotations.- Methods that come from Java, such as
peek()andpoll(), return a "platform type" (Int!). Kotlin lets you use the result as anIntwithout a null check. In real Java codepeek()andpoll()returnnullon an empty queue, which would crash on the next use as anInt. This solution never calls them on an empty heap, becausepeek()runs only after the heap has reachedkvalues. That is safe as long askis at least 1. 0 until kexcludesk, so it visits exactly the indices0tok - 1of the result.IntArray(k)creates an array ofkzeros, which the loop then overwrites.
Complexity
Time: O(n log k). Each of the n values costs at most one peek (constant time) plus one poll and one add, each O(log k) because the heap never exceeds k values. Emptying the heap at the end is k polls, which is O(k log k) and never more than the loop's cost.
Space: O(k). The heap holds at most k values. The result array also has size k.
When k is much smaller than n, this beats the O(n log n) sort. When k is close to n, the two are about equal.
Edge cases and common mistakes
- Using a min heap. A min heap keeps the smallest value on top, so you would throw away the wrong value. The comparator is what turns the queue into a max heap.
- Duplicates. The comparison is
value < peek(), strictly smaller. A value equal to the top changes nothing in the result, so skipping it is correct. - Calling
peek()too early. On an empty queue it returnsnull, which Kotlin would turn into a crash when treated as anInt. Thesize < kbranch comes first so that the heap is full beforepeek()runs. kequal to the array size. Every value is added and none is ever replaced. The result is the whole array, in descending order.- Expecting sorted output. The result comes out in descending order here, so for the example it is
[3, 2, 2, 1]. Any order is accepted.
Testing
[5, 9, 3, 6, 2, 1, 3, 2, 7, 5]withk = 4returns[3, 2, 2, 1].[4, 1]withk = 2returns[4, 1], the whole array in descending order.