Problem overview
The method kClosest receives points, an Array[Array[Int]] where each point is [x, y], and an integer k. It returns the k points closest to the origin (0, 0), sorted by distance with the closest first. Points at the same distance are ordered by x, and points with the same x by y. The answer is unique: every point left out is farther away than every point kept.
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 answer is [[3, 3], [-2, 4]].
Approach: keep the k best in a max-heap
Sorting all the points by distance and taking the first k works, but costs O(n log n). The statement asks for O(n log k) time and O(k) extra space, so we keep only the k closest points seen so far.
The right tool is a max-heap: a structure where the biggest element is always on top and adding or removing it costs O(log size). Scala's PriorityQueue is a max-heap. If the entries are ordered by distance, the top is the farthest point we are keeping, which is exactly the one to drop when a closer point arrives. Add every point, and whenever the heap holds more than k entries, remove the top. After the last point, the heap holds the k closest.
No square roots are needed. One point is closer than another exactly when its x * x + y * y is smaller, and this squared distance is a whole number. With coordinates up to 10,000 in absolute value it is at most 200,000,000, which fits in an Int.
The heap never holds more than k + 1 entries, so each operation costs O(log k).
Step by step
- Create the heap: an empty
PriorityQueue[(Int, Int, Int)](). Each entry is the tuple(dist, x, y). - Visit every point with
for (point <- points). Each point gets one chance to enter the heap. - Name the coordinates:
xispoint(0)andyispoint(1). - Compute the squared distance
dist = x * x + y * y. - Enqueue the tuple
(dist, x, y). The outer parentheses belong to the call, the inner ones build the tuple. - Trim the heap: if
heap.size > k, callheap.dequeue(). Out ofk + 1points, the farthest can't be among thekclosest. - Sort what is left:
heap.toArray.sortedcopies the entries into an array and sorts it. Tuples sort item by item, so the order is distance, thenx, theny. - Drop the distances:
closest.map { case (dist, x, y) => Array(x, y) }turns each tuple back into a point, and is the method's last expression.
The solution in Scala
In a file of your own, the code needs import scala.collection.mutable.PriorityQueue at the top.
class Solution {
def kClosest(points: Array[Array[Int]], k: Int): Array[Array[Int]] = {
val heap = PriorityQueue[(Int, Int, Int)]()
for (point <- points) {
val x = point(0)
val y = point(1)
val dist = x * x + y * y
heap.enqueue((dist, x, y))
if (heap.size > k) {
heap.dequeue()
}
}
val closest = heap.toArray.sorted
closest.map { case (dist, x, y) => Array(x, y) }
}
}
The Scala details worth a closer look:
- Tuples compare item by item.
(18, 3, 3)is smaller than(20, -2, 4)because 18 < 20; with equal distances thexdecides, then they. Scala provides this ordering for tuples of numbers, soPriorityQueue[(Int, Int, Int)]()needs no comparator. - Max-heap by default.
dequeue()removes the biggest entry, which is what we want here. A min-heap would need a reversed ordering. - The heap's internal order isn't sorted. Listing the heap, for example with
toList, gives the entries in its internal order. In the example, the heap holds(18, 3, 3)and(20, -2, 4), buttoListgives(20, -2, 4)first. That is why the method sorts before returning. - Two sets of parentheses in
enqueue((dist, x, y)). Without the inner pair, the three values would look like three arguments. case (dist, x, y) => …takes each tuple apart into its three items.distis not used on the right-hand side, which is fine.- The result type.
mapon anArray[(Int, Int, Int)]returns anArray[Array[Int]], which matches the method's return type.
Complexity
Time: O(n log k). Each of the n points is added once and the heap is trimmed at most once per point; the heap never holds more than k + 1 entries, so each operation costs O(log k). The final sort of k entries costs O(k log k), which is no more than O(n log k) because k <= n.
Space: O(k). The heap, and then the sorted array, hold k entries.
Edge cases and common mistakes
- Ties in distance. For the points
[0, 2],[2, 0],[1, 1],[0, -2],[3, 3]withk = 4, three points are at squared distance 4. They come out as[0, -2],[0, 2],[2, 0], byxand theny, after[1, 1]. The answer is[[1, 1], [0, -2], [0, 2], [2, 0]]. - Repeated points. The same point may appear more than once.
[[1, 1], [1, 1], [2, 2]]withk = 2gives[[1, 1], [1, 1]]. kequals the number of points. Nothing is ever dequeued, and the method just sorts everything.- Storing only the distance. A heap of plain
Intdistances can't give the points back. The tuple carriesxandyalong, and also provides the tie-break. - Square roots. They aren't needed, and they would bring
Doublerounding into comparisons that are exact with integers. - Overflow. For the corner
[10000, 10000]the squared distance is 200,000,000, comfortably below theIntlimit of about 2.1 billion.
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 = [[1, 1], [1, 1], [2, 2]],k = 2gives[[1, 1], [1, 1]].points = [[10000, 10000], [-10000, -10000]],k = 2gives[[-10000, -10000], [10000, 10000]].
