Closest Pair of Points — Why Your Strip Takes O(n²)
An unsorted strip turned 10K points into 30-second queries.
- Divide and conquer splits points by median x, finds closest pair in each half recursively, then checks a narrow strip around the dividing line.
- The strip’s width is δ (minimum distance from left/right halves). Only points within δ of the midline are considered.
- Each point in the strip checks at most 7 neighbours, thanks to a geometric packing argument in a 2δ×δ rectangle.
- Performance insight: the combine step runs in O(n) when the strip is sorted by y, giving overall O(n log n).
- Production insight: forgetting to sort the strip by y can silently balloon the combine step to O(n²) on dense datasets.
- Biggest mistake: assuming the 7-neighbour bound holds without sorting the strip by y-coordinate.
Given n points on a plane, find the two closest ones. Naive: check all pairs — O(n²). D&C: split points by x-coordinate, find closest pair in each half recursively, then check if a closer pair crosses the dividing line. The key insight: you only need to check a narrow strip around the dividing line, and within that strip, each point has at most 7 neighbours to check.
The closest pair problem is a beautiful example of computational geometry enabling an otherwise-quadratic combine step to run in linear time. The naive O(n^2) solution checks every pair. The D&C solution achieves O(n log n) by proving a remarkable geometric fact: in the critical 'strip' around the dividing line, each point has at most 7 other points it needs to check.
This seven-point bound — derived from packing arguments in a 2δ×δ rectangle — is the kind of geometric reasoning that separates algorithm designers from algorithm users. It is also a template for many computational geometry optimisations: use the structure of space to bound the number of interactions.
Algorithm Overview
- Sort points by x-coordinate: O(n log n)
- Recursively find closest pair in left half (δ_L) and right half (δ_R)
- δ = min(δ_L, δ_R)
- Find all points within δ of the dividing line (the strip)
- Check pairs in the strip sorted by y-coordinate
- The key: each point in the strip has at most 7 other points to check
The 7-Neighbour Proof
Why does each point in the strip have at most 7 other points to check? Consider a δ×2δ rectangle straddling the midline. In each δ×δ half, the closest pair distance is ≥ δ (we already found the best in each half). So in each δ×δ quarter, we can pack at most 4 points at distance ≥ δ from each other (a 2×2 grid). Total: 8 points maximum in the 2δ×2δ strip rectangle including the reference point itself — so at most 7 others.
This bounds the inner loop to constant work per point → O(n) combine step.
Optimisation — Pre-Sort by Y
Re-sorting the strip each time adds a log factor. The optimisation: maintain two sorted arrays throughout — one sorted by x (for dividing) and one sorted by y (for strip checking). This gives T(n) = 2T(n/2) + O(n) → O(n log n) with smaller constants.
Implementation detail: during recursion, merge the y-sorted lists of the left and right halves (like merge sort) to produce the parent's y-sorted list. The strip can then be filtered in O(|strip|) from the y-sorted list without a full sort.
Complexity Analysis and Recurrence
The recurrence for the naive version (no pre-sort): T(n) = 2T(n/2) + O(n log n). Solving via Master Theorem: a=2, b=2, f(n)=O(n log n) = O(n^(log₂2) log n) = O(n log n). This falls under Case 2 of the Master Theorem (actually a special case). The solution is O(n log² n). Because the combine step sorts the strip, it's O(n log n), leading to T(n) = 2T(n/2) + O(n log n) = O(n log² n). Wait — careful: if we sort the strip each time, the combine is O(k log k) where k ≤ n. That sums to O(n log² n) in the worst case. Pre-sorting brings combine to O(n) and recurrence to O(n log n).
- Without pre-sort: T(n) = 2T(n/2) + O(n log n) → by Master Theorem Case 2, T(n) = Θ(n log² n).
- With pre-sort: T(n) = 2T(n/2) + O(n) → by Master Theorem Case 2, T(n) = Θ(n log n).
So pre-sorting doesn't just reduce constants — it changes the asymptotic complexity.
Implementation Details and Edge Cases
Base Case: n ≤ 3. Brute force is efficient and avoids recursion overhead. Use combinations for clarity, but a triple-nested loop is fine for n=3.
Median Selection: Sort by x once, then the median is simply p[mid]. No need for linear-time selection — the sort dominates anyway.
Duplicate Points: If two points have identical coordinates, the distance is 0. The algorithm should handle this correctly because δ becomes 0 and the strip width becomes 0 — only points exactly on the midline are considered, but duplicates on the same side will be caught in the recursive step. However, if duplicates exist across halves, they may be missed because the strip is empty (δ=0). Fix: before divide, deduplicate or handle zero-distance case explicitly.
Vertical and Horizontal Lines: Sorting by x works fine. If many points share the same x, the median may be ambiguous. Use a stable sort or choose the middle index after sorting.
Floating-Point Precision: Distance calculations can suffer from floating-point errors. Use math.dist which is stable, but for comparison, prefer squared distances to avoid sqrt overhead.
Integer Coordinates: If all coordinates are integers, use squared distances to avoid sqrt entirely, only taking sqrt at the end.
Extensions and Higher Dimensions
The D&C approach generalises to k dimensions. The strip becomes a slab of width 2δ, and the neighbour bound becomes O(3^k), which grows exponentially with dimension. For 3D, each point checks at most 31 neighbours (3^3 - 1 = 26? Actually the bound is more complex). But the combinatorial explosion makes D&C impractical for dimensions > 3. For high-dimensional data, use space-filling curves or KD-trees.
Related Problems: The packing argument also applies to finding the minimal distance between two convex polygons, or computing the diameter of a point set using rotating calipers.
Practical Note: In production, for 2D and 3D, the D&C algorithm is the gold standard. For higher dimensions, approximate methods (like LSH) are used.
Closest Pair Blowup in Geospatial Query
strip.sort(key=lambda p: p[1]) and enforced the 7-point check with break condition on y-difference. Profiling confirmed O(n log n) after the fix.- The geometric bound only applies when points are sorted by y in the strip.
- Always verify assumptions about pre-sorting when using D&C combine steps.
- Profile before assuming O(n) — a missing sort can silently degrade performance.
Key takeaways
Common mistakes to avoid
5 patternsForgetting to sort the strip by y-coordinate
strip.sort(key=lambda p: p[1]) before the inner loop. Alternatively, maintain a y-sorted list during recursion to avoid the sort entirely.Using '<=' instead of '<' for strip width
< delta (strict less than) when filtering points within the strip. The strip should include points strictly less than δ away from the midline.Not handling duplicate points
Base case too small (n ≤ 1) or too large (n ≤ 10)
Not updating delta after finding a closer pair in the strip
delta after each closer pair found: if d < delta: best = ...; delta = d. Then the inner loop's break condition uses the updated delta.Interview Questions on This Topic
Why does each point in the strip need at most 7 comparisons?
Frequently Asked Questions
That's Recursion. Mark it forged?
4 min read · try the examples if you haven't