Problem overview
The function kClosest(points: Array<IntArray>, k: Int): Array<IntArray> receives points [x, y] on a plane and returns the k points closest to the origin (0, 0). The answer is sorted: closest first, points at the same distance ordered by x, and points with the same x ordered by y. The answer is guaranteed to be unique, so every point left out is strictly farther away than every point in the answer.
For points = [[3, 3], [5, -1], [-2, 4]] and k = 2, the squared distances are 18, 26 and 20. The two closest are [3, 3] and [-2, 4], in that order, so the result is [[3, 3], [-2, 4]].
For [[0, 2], [2, 0], [1, 1], [0, -2], [3, 3]] and k = 4 the result is [[1, 1], [0, -2], [0, 2], [2, 0]]. Three points are at distance 2, so they are ordered by x, and the two with x = 0 by y.
The lesson asks for O(n log k) time and O(k) extra space. Sorting all n points would cost O(n log n).
Approach: a heap that keeps only the k closest
Two facts shape the solution.
Squared distances are enough. A point is closer than another exactly when its x * x + y * y is smaller, so no square root is needed. The value stays a whole number, and with coordinates up to 10,000 in size it is at most 200,000,000, which fits in an Int.
Only the k closest need to be remembered. Scan the points once and keep a collection of the best k seen so far. When a new point arrives, it joins the collection, and if the collection now holds k + 1 points, the farthest of them is certainly not among the final k, so it is dropped. The collection must therefore answer "which point is the farthest?" quickly, and that is the job of a max-heap: the biggest distance sits on top, and adding or removing costs O(log k) because the heap never holds more than k + 1 entries.
For example, with k = 2 on the points [3, 3], [5, -1], [-2, 4], each entry written as [distance, x, y]:
after (3, 3): kept=[[18, 3, 3]]
after (5, -1): kept=[[18, 3, 3], [26, 5, -1]]
after (-2, 4): dropped=[26, 5, -1] kept=[[18, 3, 3], [20, -2, 4]]
When the third point arrives, the heap has three entries, and the farthest, 26, is removed. The scan costs n heap operations of O(log k) each, which gives O(n log k).
A heap is only partly ordered, so the k survivors still have to be sorted at the end. That costs O(k log k), which is no more than the rest of the work since k <= n.
Step by step
- Create the heap. It is a
PriorityQueue<IntArray>built withcompareByDescending { it[0] }. Each entry isintArrayOf(dist, x, y), and the comparator looks at the distance, so the biggest one is on top. - Loop over the points, taking each one apart as
(x, y). - Compute the squared distance
distasx * x + y * y. - Add the entry
intArrayOf(dist, x, y). The distance is stored with the point because it is what the heap orders by. - Trim the heap. If
heap.size > k, callheap.poll(). It removes the top, the farthest point. After each point, the heap holds thekclosest seen so far. - Copy the survivors into a list with
heap.toMutableList(). The list is in heap order, not sorted. - Sort the list with three keys in order: distance, then
x, theny. Each later key matters only when the earlier ones are equal. - Drop the distances by mapping each entry to
intArrayOf(it[1], it[2]), then convert the list to an array withtoTypedArray().
The solution in Kotlin
class Solution {
fun kClosest(points: Array<IntArray>, k: Int): Array<IntArray> {
val heap = PriorityQueue<IntArray>(compareByDescending { it[0] })
for ((x, y) in points) {
val dist = x * x + y * y
heap.add(intArrayOf(dist, x, y))
if (heap.size > k) {
heap.poll()
}
}
val closest = heap.toMutableList()
closest.sortWith(compareBy<IntArray>({ it[0] }, { it[1] }, { it[2] }))
return closest.map { intArrayOf(it[1], it[2]) }.toTypedArray()
}
}
Kotlin details worth a look:
PriorityQueueis Java's. Kotlin has no heap of its own, so it usesjava.util.PriorityQueue, which is a min-heap: the smallest element by the comparator is on top. The comparatorcompareByDescending { it[0] }reverses the order on the distance, which turns it into a max-heap. In a file of your own, addimport java.util.PriorityQueue.- Destructuring an
IntArray.for ((x, y) in points)uses the array'scomponent1()andcomponent2()functions, which return its first two elements. A point with more than two numbers would simply have the rest ignored. addandpoll.addinserts an element.pollremoves and returns the top, ornullwhen the queue is empty. Here the heap is never empty whenpollruns, and the result isn't needed.- The heap is not sorted. Iterating a
PriorityQueue, which is whattoMutableList()does, doesn't give sorted order: for entries with distances 1 to 6 added in order, the list comes out as[6, 4, 5, 1, 3, 2]. Only the top is guaranteed. This is why the final sort is needed. compareBywith several selectors.compareBy<IntArray>({ it[0] }, { it[1] }, { it[2] })compares by the first selector and goes to the next only on a tie. The explicit type argument<IntArray>is needed because the lambdas alone don't tell the compiler what they receive.- Converting back.
mapon a list returns aList, and the function must return anArray<IntArray>, sotoTypedArray()finishes the job. - Why the answer's uniqueness matters. The heap decides which point to drop by distance alone. If two points tied at the cut-off, it could drop either one. The lesson guarantees there is no such tie.
Complexity
Time: O(n log k). Each of the n points is added once and the heap is trimmed when it passes k entries, so it never holds more than k + 1 entries and each operation costs O(log k). The final sort of k entries costs O(k log k).
Space: O(k) extra. The heap, the list copy and the returned array all hold at most k entries.
Edge cases and common mistakes
- The origin itself.
[[0, 0]]withk = 1returns[[0, 0]]. Its distance is 0, which is a valid value. - Duplicate points.
[[1, 1], [1, 1], [5, 5]]withk = 2returns[[1, 1], [1, 1]]. Both copies are kept, since each is its own entry in the heap. - Largest coordinates.
[[10000, 10000], [-10000, -10000], [0, 1]]withk = 3returns[[0, 1], [-10000, -10000], [10000, 10000]]. The two far points are at the same distance, so the one with the smallerxcomes first, andx * x + y * yreaches 200,000,000, which still fits in anInt. - Using a min-heap by mistake. With the farthest point not on top,
pollwould drop the closest point instead. - Skipping the final sort. The heap's iteration order isn't distance order, so the tests would fail.
- Forgetting the tie-breakers. Sorting only by distance leaves points at equal distance in an arbitrary order.
- Using square roots. They add nothing here. Comparing squared distances gives the same order without floating-point values.
Testing
points = [[1, 3], [-2, 2]],k = 1gives[[-2, 2]].points = [[3, 3], [5, -1], [-2, 4]],k = 2gives[[3, 3], [-2, 4]].points = [[0, 2], [2, 0], [1, 1], [0, -2], [3, 3]],k = 4gives[[1, 1], [0, -2], [0, 2], [2, 0]].points = [[0, 0]],k = 1gives[[0, 0]].points = [[1, 1], [1, 1], [5, 5]],k = 2gives[[1, 1], [1, 1]].
