Problem overview
The function kClosest receives a vector<vector<int>> named points, where each points[i] is {x, y}, and an int named k. It returns the k points closest to the origin (0, 0), as a vector<vector<int>> sorted by distance, closest first. Points at the same distance come in order of x, and points with the same x in order of y. The answer is unique: every point left out is farther away than every point in the answer.
For points = {{1, 3}, {-2, 2}} and k = 1 the answer is [[-2,2]]: {1, 3} is √10 from the origin and {-2, 2} is √8. For {{3, 3}, {5, -1}, {-2, 4}} and k = 2 the squared distances are 18, 26 and 20, so the answer is [[3,3],[-2,4]]. With {{0, 2}, {2, 0}, {1, 1}, {0, -2}, {3, 3}} and k = 4, the answer is [[1,1],[0,-2],[0,2],[2,0]]: the three points at distance 2 are ordered by x, then y, and {3, 3} is left out.
The statement asks for O(n log k) time and O(k) extra space, where n is the number of points. Sorting all the points by distance would take O(n log n).
Approach: keep the k best in a max-heap
The distance to the origin is √(x² + y²), but no square root is needed. A point is closer than another exactly when its squared distance is smaller, and the 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.
To keep only the best k points while scanning, use a max-heap of the points kept so far. std::priority_queue is a max-heap by default: its top is the largest element, here the farthest point kept. Each new point goes in, and when the heap holds more than k points the top is removed. Out of k + 1 points the farthest can't be among the k closest, so dropping it is always safe. After the last point, the heap holds exactly the k closest.
Each push and pop costs O(log k), because the heap never holds more than k + 1 entries, and there are n pushes, so the scan takes O(n log k).
The heap entries are array<int, 3> values {dist, x, y}. A std::array compares element by element, so entries are ordered by distance first, then x, then y, which is exactly the order the answer needs. For {{3, 3}, {5, -1}, {-2, 4}} with k = 2 the scan goes like this:
after push (3,3): size 1 top dist 18
after push (5,-1): size 2 top dist 26
after push (-2,4): size 3 top dist 26 -> pop dist 26
The third push makes the heap too big, and the top, the point at squared distance 26, is dropped. The heap is left with the points at 18 and 20.
A heap is only partly ordered, so the kept entries still have to be sorted. They come out of the heap farthest first, so the code collects them into a vector and sorts it. That sort handles only k entries, so it costs O(k log k), which never exceeds O(n log k) because k <= n.
Step by step
- Declare the heap:
priority_queue<array<int, 3>> heap, a max-heap of{dist, x, y}entries. - Visit every point with a range-based loop,
for (auto& point : points). The&reads each point in place instead of copying it. - Read the coordinates into
xandy. - Compute the squared distance
distasx * x + y * y. - Push
{dist, x, y}. The braces build thearray<int, 3>in the call. - Trim the heap: if
heap.size() > k, callheap.pop(). After every point the heap holds thekclosest seen so far. - Declare
closest, avector<array<int, 3>>, to take the entries out. Apriority_queuecan only be read from its top. - Loop while
!heap.empty(), which runsktimes. - Move each entry out:
push_back(heap.top()), thenheap.pop(). In C++pop()returns nothing, so the top is read first and removed second. - Sort
closestfrombegin()toend(). The default order on arrays is distance, thenx, theny. - Declare
result, avector<vector<int>>. - Loop over
closestwithauto& entry. - Convert each entry into a point with
result.push_back({entry[1], entry[2]}). The distance at index 0 has done its job. - Return
result.
The solution in C++
class Solution {
public:
vector<vector<int>> kClosest(vector<vector<int>>& points, int k) {
priority_queue<array<int, 3>> heap;
for (auto& point : points) {
int x = point[0], y = point[1];
int dist = x * x + y * y;
heap.push({dist, x, y});
if (heap.size() > k) {
heap.pop();
}
}
vector<array<int, 3>> closest;
while (!heap.empty()) {
closest.push_back(heap.top());
heap.pop();
}
sort(closest.begin(), closest.end());
vector<vector<int>> result;
for (auto& entry : closest) {
result.push_back({entry[1], entry[2]});
}
return result;
}
};
The C++ details worth a closer look:
priority_queueis a max-heap. With no comparator argument it keeps the largest element on top, which is what this problem needs. Finding the smallest values with a heap of sizekuses the largest of them as the gatekeeper.array<int, 3>compares lexicographically.operator<compares the first elements, then the second if they are equal, then the third. A heap orsortover these arrays needs no custom comparator.pop()returns nothing. Readtop()first, then callpop(). The code copies the entry intoclosestbefore removing it.heap.size() > kcompares an unsigned size with anint. The constraints keepkat 1 or more, so the comparison is correct, though some compilers warn about the mix.- No overflow.
x * x + y * yis at most 200,000,000, well inside theintrange, so the squared distance never needslong longhere. Signed overflow would be undefined behaviour. - Brace initialisation.
heap.push({dist, x, y})andresult.push_back({entry[1], entry[2]})build anarray<int, 3>and avector<int>directly in the call.
Complexity
Time: O(n log k). The loop over the points does one push and at most one pop per point, each O(log k) because the heap never holds more than k + 1 entries. Emptying the heap costs O(k log k), and sorting the k entries costs O(k log k). Since k <= n, the total is O(n log k).
Space: O(k) extra. The heap holds at most k + 1 entries, and closest and result hold k each.
Edge cases and common mistakes
- Using a min-heap. A heap whose top is the closest point would need all
npoints inside it. Keeping the farthest point on top is what allows the heap to stay at sizek. - Skipping the final sort. The heap pops points farthest first, and its internal order isn't sorted anyway. The answer needs the closest first, in the order given by the statement.
- Taking square roots. Square roots add nothing and bring in floating-point rounding. Comparing squared distances is exact.
- Losing the tie-break. Ordering entries by distance alone gives no rule for equal distances. Keeping
xandyin the entry makes the tie-break part of the comparison. With the points{0, 2},{2, 0}and{0, -2}, all at squared distance 4, the result is[0,-2],[0,2],[2,0]. - Duplicates. The same point can appear more than once.
{{1, 1}, {1, 1}, {2, 2}}withk = 2gives[[1,1],[1,1]]. kequal to the number of points. Nothing is ever popped.{{3, 3}, {5, -1}, {-2, 4}}withk = 3gives[[3,3],[-2,4],[5,-1]], all the points sorted.- Coordinates at the limits.
{{10000, 10000}, {-10000, -10000}}withk = 2gives[[-10000,-10000],[10000,10000]]: equal distances, so the smallerxcomes first.
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 = {{5, 5}},k = 1gives[[5,5]].points = {{1, 1}, {1, 1}, {2, 2}},k = 2gives[[1,1],[1,1]].
