Closest Pair of Points
Find the two closest points among n points in the plane in O(n log n) by splitting on x, recursing, and checking only a thin strip around the split line.
Overview
Given n points, the brute force compares all n(n−1)/2 pairs in O(n²). The divide-and-conquer algorithm sorts the points by x, splits at the median vertical line, recursively finds the closest pair on each side (distance dL, dR, take d = min), and then looks for a closer pair crossing the line. The crucial observation is that any crossing pair with distance < d lies in the strip |x − x_mid| < d, and within that strip, sorted by y, each point needs to be compared with at most 7 following points.
With the points also kept sorted by y (merge the two halves' y-orders during combine, as in Merge Sort), the combine step is O(n) and the recurrence T(n) = 2T(n/2) + O(n) gives O(n log n). Sorting by y inside every call instead gives O(n log² n), which is usually acceptable in interviews.
Intuition
A mental model before the formal terms.
Draw a vertical line through the middle of the points. The closest pair is either entirely left, entirely right, or has one point on each side. The first two cases are the recursive calls. For the third, you already know the best distance d so far — so only points within d of the line can matter; everything else is too far to cross.
Inside that strip, points cannot be packed arbitrarily: on each side of the line, every two points are at least d apart (that is what the recursion proved). So a d × 2d rectangle can hold at most 8 points. Walking the strip in y order, once a point is more than d below you, nothing further down can beat d — you only ever look at a handful of neighbors.
How it works
- Sort points by
xonce (Px) and byyonce (Py). - Recurse: if
n ≤ 3, brute force. Otherwise splitPxat the medianx_mid; splitPyintoLy/Ryin one pass by comparing each point'sxwithx_mid(preservingy-order). - Let
d = min(closest(L), closest(R)). - Build the strip: all points of
Pywith|x − x_mid| < d, already iny-order. - For each strip point
i, compare with strip pointsj > iwhiley[j] − y[i] < d(at most 7 comparisons); updated. - Return
d(and the pair that achieved it).
Why it works
Any pair closer than d with points on opposite sides has both points within horizontal distance d of the line, hence in the strip; the strip scan finds it.
Packing argument: consider a d × 2d rectangle straddling the line, with the point p on its bottom edge. Each half is a d × d square whose points are pairwise ≥ d apart (they come from one side), so each half holds at most 4 points (place them at the corners; a 5th would be within d of one). Thus at most 8 points, i.e. p and 7 others; any point more than d above p in y is outside the rectangle and cannot be within d.
Since each strip point does O(1) work and splitting Py is linear, the combine step is O(n) and T(n) = 2T(n/2) + O(n) = O(n log n) by the Master theorem (case 2).
Recognition
How to tell a problem wants this.
- "Closest / nearest two points", "minimum distance between any two of
npoints", withnup to10^5..10^6(brute forcen²is too slow). - Points in the plane where a split by one coordinate leaves a bounded-width interaction zone.
- Any problem phrased as "the answer is either in the left half, the right half, or crosses the middle" with a geometric pruning bound.
Interactive visualization
Play, step, change the input. ← → and space work too.
No interactive visualization for this topic yet
Related visualizations are linked under Related.
Pseudocode
1closest(Px, Py):2 if |Px| <= 3: return brute force3 mid = |Px| / 2; x_mid = Px[mid].x4 Lx, Rx = Px[:mid], Px[mid:]5 Ly, Ry = split Py by x < x_mid (keep y order)6 d = min(closest(Lx, Ly), closest(Rx, Ry))7 strip = [p in Py if |p.x - x_mid| < d]8 for i in strip: for j = i+1 while strip[j].y - strip[i].y < d:9 d = min(d, dist(strip[i], strip[j]))10 return dImplementations
1import math2from typing import NamedTuple3 4# Closest pair of points in the plane in O(n log n): sort by x, split, recurse5# on both halves, then check only a thin strip around the split line — where a6# geometric argument bounds the work at a constant number of comparisons.7 8 9class Point(NamedTuple):10 x: float11 y: float12 13 14def dist(a: Point, b: Point) -> float:15 return math.hypot(a.x - b.x, a.y - b.y)16 17 181 · Brute force for tiny ranges; the recursion bottoms out here19def brute_force(pts: list[Point], lo: int, hi: int) -> float:20 best = math.inf21 for i in range(lo, hi):22 for j in range(i + 1, hi):23 best = min(best, dist(pts[i], pts[j]))24 return best25 26 272 · Recurse on both halves; d is the better of the two28def closest_rec(by_x: list[Point], lo: int, hi: int) -> float:29 if hi - lo <= 3:30 return brute_force(by_x, lo, hi)31 mid = (lo + hi) // 232 mid_x = by_x[mid].x33 d = min(closest_rec(by_x, lo, mid), closest_rec(by_x, mid, hi))34 353 · Only points within d of the split line can beat d36 strip = [p for p in by_x[lo:hi] if abs(p.x - mid_x) < d]37 strip.sort(key=lambda p: p.y)38 394 · Within the strip, sorted by y, at most 7 later points can be closer40 # than d — because a d-by-2d rectangle holds at most 8 points that are all41 # at least d apart from each other42 for i, pi in enumerate(strip):43 for j in range(i + 1, len(strip)):44 if strip[j].y - pi.y >= d:45 break46 d = min(d, dist(pi, strip[j]))47 return d48 49 505 · Sort by x once, then recurse51def closest_pair(pts: list[Point]) -> float:52 if len(pts) < 2:53 return math.inf54 by_x = sorted(pts, key=lambda p: p.x)55 return closest_rec(by_x, 0, len(by_x))math.hypot(dx, dy)computes the distance without intermediate overflow, and is the idiomatic Python spelling.strip = [p for p in by_x[lo:hi] if abs(p.x - mid_x) < d]builds the strip in one comprehension, thoughby_x[lo:hi]copies the slice.- The inner loop uses an explicit
breakbecause Pythonforhas no compound condition — the C-stylefor (...; cond; ...)has no direct equivalent. for i, pi in enumerate(strip)binds the outer point once, avoiding repeated indexing in the inner loop.Point(NamedTuple)gives an immutable, tuple-backed record; note thatsorted(pts)without a key would order by(x, y), which happens to be the x-order needed.
The by_x[lo:hi] slice at every level adds an O(n) copy per level, on top of the strip sort.
math.hypothandles extreme magnitudes without overflow and accepts any number of arguments since 3.8 (so it generalises to n dimensions).math.dist(p, q)(3.8+) computes the Euclidean distance between two point sequences directly, which is shorter still.- Python
forhas no compound condition, so the early exit must be an explicitbreak— the one place the loop reads less directly than in the other three languages. scipy.spatial.KDTree.querysolves the practical version of this problem and is the right tool outside a teaching context.
- Slicing
by_x[lo:hi]at every level and paying an extra O(n) copy per level. - Forgetting the
breakand making the strip pass quadratic. - Relying on
sorted(pts)to sort by x — it does, but only becauseNamedTuplecompares field by field, which is fragile if a field is ever reordered.
- Compound loop conditions: C++ and JS/TS put the early exit in the
forheader, while Python needs an explicitbreak— the only structural difference between the four versions. - Distance helpers: Python has
math.hypotandmath.dist, C++ hasstd::hypot, JS/TS haveMath.hypot— all overflow-safe and all slower than the directsqrt(dx*dx + dy*dy). - Immutable point records come free in Python (
NamedTuple) and must be arranged in the other three; the flip side is thatNamedTuplealso makessorted(pts)silently order by(x, y). - Every version here is O(n log^2 n) because it re-sorts the strip per level; the refinement to O(n log n) is identical in all four and is noted rather than claimed.
Complexity
T(n) = 2T(n/2) + O(n). Re-sorting the strip by y in each call gives O(n log² n). Brute force is O(n²).
Compare growth rates in the Complexity Explorer →When to use — and when not to
- Exact closest pair for
nbeyond a few thousand in the Euclidean plane. - As a template for other "crossing-the-split" geometric problems where a distance bound limits the interaction zone.
n ≤ ~2000— theO(n²)brute force is simpler and fast enough.- Points arrive online or move — the algorithm is batch-only; use a spatial grid / k-d tree instead.
- Non-Euclidean metrics where the packing argument fails (the 7-neighbor bound relies on the geometry of the
d × 2drectangle).
Alternatives
Common mistakes
- Building the strip from
Px(x-sorted) instead ofPyand then scanning withouty-order — the 7-neighbor bound only holds when walking iny. - Using
≤ dinstead of< dinconsistently, or forgetting to updatedinside the strip loop (later strip points depend on the tightened bound). - Splitting
Pyby comparingx < x_midalone when several points sharex_mid— the halves no longer matchLx/Rx; split by membership or by index rank. - Recursing until
n == 1— an empty or singleton half yields an infinite distance, which is fine, but the base casen ≤ 3avoids the degenerate strip.
Interview patterns
- State the three cases (left, right, crossing) and the packing argument — that is the whole interview.
- Follow-up: "why 7?" Draw the
d × 2drectangle with 8 corner points. - Follow-up: "how to get
n log ninstead ofn log² n?" Merge they-orders instead of sorting.
- Where does O(n log n) come from?Beginner
- Average case versus worst caseIntermediate
- Kth Largest Element in an ArrayIntermediate