973. K Closest Points to Origin

Find the k points nearest to the origin without sorting the whole array. A max-heap capped at size k does it in O(n log k) — the root always holds the farthest of your current k-best, so you know exactly when to evict.

MediumMax-Heap of size kTop-K selectionTypeScript

PROBLEM What we're solving

Given a list of 2-D points and an integer k, return the k points closest to the origin (0, 0). Distance is Euclidean, but we never need the square root — we compare squared distances.

Concrete example. Points [[1,3],[-2,2],[5,8],[0,1]], k=2. Squared distances: 1²+3²=10, (-2)²+2²=8, 25+64=89, 0+1=1. The two smallest are [0,1] (d²=1) and [-2,2] (d²=8). Answer: [[-2,2],[0,1]](order doesn't matter).

KEY IDEA Use a max-heap to track the k smallest distances

Insight → to keep the k smallest things seen so far, maintain a max-heap of size k. The root is always the largest among your k-best — the threshold to beat. When a new point is closer than the root, swap it in and sift down. At the end the heap is the answer. No sqrt needed: comparing squared distances preserves order.

RECIPE Max-heap of size k, root = farthest of k-best

  • 0 · Init. Start with an empty max-heap keyed on x²+y². No sqrt — squaring preserves the ordering.
  • 1 · For each point p, push it into the heap. sift-up to restore the max-heap property (O(log k)).
  • 2 · If heap size exceeds k, pop the root. The root is the farthest point currently in the heap — exactly what we want to evict. Replace root with the last element and sift-down (O(log k)).
  • 3 · Return the heap. After all n points, the heap holds exactly the k closest. No extra work needed.
Classic confusion → people instinctively reach for a min-heap, then try to extract k elements at the end. That works but costs O(n log n) to build the full heap + O(k log n) to pop. The max-heap of size k approach does O(n log k) total — faster when k << n. Using a min-heap of size k is the wrong polarity; you'd end up evicting the closest points instead of the farthest ones.

COST Complexity & alternatives

Sort all, take first k
O(n log n)
Simple, but sorts points you never need.
Max-heap of size k
O(n log k)
O(k) space; each point costs at most one push + one pop.

Alternatives

Quickselect (partial sort) achieves O(n) average time at the cost of randomisation and more complex code — useful if k is large and you need the best asymptotic. For most interview settings the heap solution is preferred: predictable O(n log k), trivially correct, easy to explain.

Space: the heap always holds at most k points, so space is O(k).

Pattern transfer →“keep the k smallest/largest seen so far” is the canonical max/min-heap-of-size-k pattern. It appears in K Closest Points to Origin (this problem), Top K Frequent Elements (LC 347), Kth Largest Element in a Stream (LC 703), Find K Pairs with Smallest Sums (LC 373), and Kth Largest Element in an Array (LC 215).

RUN IT Max-heap of size k — keep the k closest

step 0 / 5
STARTWe want the 2 closest points to the origin from 4 candidates. Strategy: maintain a max-heap of size 2 keyed on squared distance.
1function kClosest(points: number[][], k: number): number[][] {
2 // Max-heap stored as an array: root holds the FARTHEST point among our k best.
3 const heap: number[][] = [];
4
5 const dist2 = (p: number[]) => p[0] * p[0] + p[1] * p[1];
6
7 const swap = (i: number, j: number) => {
8 [heap[i], heap[j]] = [heap[j], heap[i]];
9 };
10
11 const siftUp = (i: number) => {
12 while (i > 0) {
13 const parent = Math.floor((i - 1) / 2);
14 if (dist2(heap[parent]) < dist2(heap[i])) {
15 swap(parent, i);
16 i = parent;
17 } else break;
18 }
19 };
20
21 const siftDown = (i: number, size: number) => {
22 while (true) {
23 let largest = i;
24 const l = 2 * i + 1, r = 2 * i + 2;
25 if (l < size && dist2(heap[l]) > dist2(heap[largest])) largest = l;
26 if (r < size && dist2(heap[r]) > dist2(heap[largest])) largest = r;
27 if (largest === i) break;
28 swap(i, largest);
29 i = largest;
30 }
31 };
32
33 for (const p of points) {
34 heap.push(p);
35 siftUp(heap.length - 1);
36
37 if (heap.length > k) {
38 // Root is the farthest; replace it with the last element and sift down.
39 heap[0] = heap.pop()!;
40 siftDown(0, heap.length);
41 }
42 }
43
44 return heap;
45}
points(1,3)d²=10(-2,2)d²=8(5,8)d²=89(0,1)d²=1
State
p
d² (p)
0 / 2
heap size
(empty)
heap (max-top)
topD²
evicted
(empty)
heap (all)
current point p / d²(p)in heap (k-best so far)heap root (farthest among k-best)evicted pointheap size
slowfast

TYPESCRIPT The solution, annotated

kClosest.ts
function kClosest(points: number[][], k: number): number[][] {
  // Max-heap stored as an array: root holds the FARTHEST point among our k best.
  const heap: number[][] = [];

  const dist2 = (p: number[]) => p[0] * p[0] + p[1] * p[1];

  const swap = (i: number, j: number) => {
    [heap[i], heap[j]] = [heap[j], heap[i]];
  };

  const siftUp = (i: number) => {
    while (i > 0) {
      const parent = Math.floor((i - 1) / 2);
      if (dist2(heap[parent]) < dist2(heap[i])) {
        swap(parent, i);
        i = parent;
      } else break;
    }
  };

  const siftDown = (i: number, size: number) => {
    while (true) {
      let largest = i;
      const l = 2 * i + 1, r = 2 * i + 2;
      if (l < size && dist2(heap[l]) > dist2(heap[largest])) largest = l;
      if (r < size && dist2(heap[r]) > dist2(heap[largest])) largest = r;
      if (largest === i) break;
      swap(i, largest);
      i = largest;
    }
  };

  for (const p of points) {
    heap.push(p);
    siftUp(heap.length - 1);

    if (heap.length > k) {
      // Root is the farthest; replace it with the last element and sift down.
      heap[0] = heap.pop()!;
      siftDown(0, heap.length);
    }
  }

  return heap;
}

Reading it block by block

Lines 2–3 — heap array + dist2 helper. We implement the max-heap inline as a plain array. dist2 returns x²+y². Comparing squared distances is equivalent to comparing Euclidean distances (squaring is monotone for non-negatives), so we skip the Math.sqrt entirely.
Lines 5–24 — siftUp and siftDown. Standard binary heap operations keyed on dist2. siftUpmoves a newly inserted element toward the root while it is farther than its parent (max-heap invariant: parent ≥ children by distance). siftDown restores the invariant after the root is replaced.
Lines 26–34 — main loop. For each point we push it unconditionally and sift up. If the heap now exceeds size k, the root (the farthest of our k+1 candidates) must be evicted: we overwrite it with the last element and sift down. This keeps the heap at exactly size k after each iteration where the heap was full.
Line 37 — return heap.The remaining heap entries are exactly the k closest points — no further extraction needed. The problem says order doesn't matter, so returning the raw heap array is valid.
Complexity → Time: each of the n points triggers at most one push + one pop, each costing O(log k) heap operations → O(n log k) total. Space: the heap holds at most k+1 points at any moment → O(k).

INTERVIEWFollow-ups they'll ask

  • “What if k equals n?” The heap stores every point and we never evict — equivalent to O(n log n) heap-sort. In that limit, just sort the array directly.
  • “Can you solve it in O(n) average time?” Yes — use Quickselect (partition around a pivot like QuickSort, recurse only on the relevant half). Average O(n), but worst-case O(n²) without randomisation.
  • “Return the k-th closest distance, not the points.” Same structure — just track the k-th largest distance seen (the heap root) and return Math.sqrt(dist2(heap[0])) at the end.
  • “What if the input is a stream (you can't store all points)?” The max-heap-of-size-k approach handles this perfectly — you never store more than k points in memory at once.
  • “3D points?” Change dist2 to x²+y²+z². The rest of the algorithm is identical.

OPTIMAL Max-Heap of size k

kClosest.ts
function kClosest(points: number[][], k: number): number[][] {
  // Max-heap stored as an array: root holds the FARTHEST point among our k best.
  const heap: number[][] = [];

  const dist2 = (p: number[]) => p[0] * p[0] + p[1] * p[1];

  const swap = (i: number, j: number) => {
    [heap[i], heap[j]] = [heap[j], heap[i]];
  };

  const siftUp = (i: number) => {
    while (i > 0) {
      const parent = Math.floor((i - 1) / 2);
      if (dist2(heap[parent]) < dist2(heap[i])) {
        swap(parent, i);
        i = parent;
      } else break;
    }
  };

  const siftDown = (i: number, size: number) => {
    while (true) {
      let largest = i;
      const l = 2 * i + 1, r = 2 * i + 2;
      if (l < size && dist2(heap[l]) > dist2(heap[largest])) largest = l;
      if (r < size && dist2(heap[r]) > dist2(heap[largest])) largest = r;
      if (largest === i) break;
      swap(i, largest);
      i = largest;
    }
  };

  for (const p of points) {
    heap.push(p);
    siftUp(heap.length - 1);

    if (heap.length > k) {
      // Root is the farthest; replace it with the last element and sift down.
      heap[0] = heap.pop()!;
      siftDown(0, heap.length);
    }
  }

  return heap;
}
Complexity → Time: each of the n points triggers at most one push + one pop, each costing O(log k) heap operations → O(n log k) total. Space: the heap holds at most k+1 points at any moment → O(k).

ALT 1 Brute force — sort every point by distance, take k

O(n log n) time · O(n) space

Compute each point's squared distance, sort the whole array by it, and slice off the first k. No heap bookkeeping — just one full sort.

approach-2.ts
function kClosest(points: number[][], k: number): number[][] {
  const dist2 = (p: number[]) => p[0] * p[0] + p[1] * p[1];
  return [...points]
    .sort((a, b) => dist2(a) - dist2(b))
    .slice(0, k);
}
Note → Sorting all n points is O(n log n) even though we only need the closest k. A bounded max-heap of size k gives O(n log k) time and O(k) space, and Quickselect can reach O(n) average.

MNEMONIC The one-liner

"Max-heap of size k: the root is your worst keep. If a newcomer beats it, kick the root out."

TRIGGERS When you see ___ → reach for ___

"k closest / k smallest"max-heap of size k (root = threshold to beat)
distance comparison (no absolute values needed)compare x²+y² — skip sqrt
"streaming / online" + top-kmax-heap of size k (never store more)
"k largest" variantflip to min-heap of size k (root = smallest of the large)

SKELETON The reusable shape

skeleton.ts
const heap: number[][] = [];
const dist2 = (p: number[]) => p[0]*p[0] + p[1]*p[1];
// siftUp / siftDown as a max-heap keyed on dist2

for (const p of points) {
  heap.push(p);
  siftUp(heap.length - 1);
  if (heap.length > k) {
    heap[0] = heap.pop()!;
    siftDown(0, heap.length);
  }
}
return heap;

FLASHCARDS Tap to flip

Why use a MAX-heap (not min) to find the k CLOSEST points?
We want to evict the farthest point whenever the heap overflows. The max-heap puts the farthest at the root, so popping costs O(log k) and removes exactly the right element.
tap to flip
1 / 7
Answer to reveal explanations. No penalty for retries.Score: 0 / 9
QUESTION 01
Time complexity of the max-heap-of-size-k approach?
QUESTION 02
Points [[1,3],[-2,2],[5,8],[0,1]], k=2. Which two points are returned?
QUESTION 03
Why does the max-heap evict from the ROOT rather than appending or inserting?
QUESTION 04
Why can you skip Math.sqrt when comparing distances?
QUESTION 05
A min-heap is used instead of a max-heap to collect the k closest points. What goes wrong?
QUESTION 06
What is the space complexity of the max-heap approach?
QUESTION 07
Which of the following problems is NOT solved by the same max-heap-of-size-k pattern?
QUESTION 08
#973 · K Closest Points to OriginMaintain a max-heap of size k keyed on squared Euclidean distance (no sqrt needed); on each new point, push it and pop if the heap exceeds size k, keeping the k closest. O(n log k).Which algorithmic approach does this primarily use?
QUESTION 09
#973 · K Closest Points to OriginMaintain a max-heap of size k keyed on squared Euclidean distance (no sqrt needed); on each new point, push it and pop if the heap exceeds size k, keeping the k closest. O(n log k).Which implementation correctly solves it?