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.
✦ Definition~90s read
What is Closest Pair of Points?
The Closest Pair of Points problem asks: given n points in a plane, find the pair with the smallest Euclidean distance. The naive O(n²) brute force is trivial, but the classic divide-and-conquer solution promises O(n log n) — yet many implementations accidentally regress to O(n²) in the strip-comparison phase.
★
Given n points on a plane, find the two closest ones.
This happens when you naively compare every point in the strip against every other, missing the geometric constraint that limits comparisons to at most 7 neighbors per point. The problem is a classic trap because it looks straightforward but requires a precise proof and careful implementation to achieve the advertised complexity.
The algorithm splits points by x-coordinate, recursively finds the minimum distance δ in left and right halves, then examines a vertical strip of width 2δ around the midline. The critical insight — and the source of the O(n²) trap — is that within this strip, any point can only be within δ of at most 7 other points when sorted by y-coordinate.
This 7-neighbor bound relies on a packing argument: a δ×2δ rectangle can contain at most 8 points with pairwise distance ≥ δ, and one of those is the point itself. Without pre-sorting the strip by y and limiting comparisons to the next 7 points, you end up comparing each point against all others in the strip, which can be O(n) per point in worst case.
In practice, the optimization that saves you is pre-sorting all points by y-coordinate once (O(n log n)) and then filtering the strip in linear time during each recursive step. The recurrence T(n) = 2T(n/2) + O(n) yields O(n log n) overall. Edge cases include duplicate points (distance zero), floating-point precision, and handling the strip when δ is zero.
Real-world applications range from collision detection in physics engines to GIS proximity queries, where the O(n log n) bound matters at scales above ~10,000 points. Below that, brute force often wins due to lower constant factors.
Plain-English First
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.
⚙ Browser compatibility
Latest versions — ✓ supported
Chrome
Firefox
Safari
Edge
✓
✓
✓
✓
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.
Why the Closest Pair of Points Problem Is a Divide-and-Conquer Trap
The closest pair of points problem asks: given n points in a plane, find the pair with the smallest Euclidean distance. The naive O(n²) solution checks every pair — fine for 1,000 points, fatal for 100,000. The divide-and-conquer solution achieves O(n log n) by recursively splitting the set, solving each half, and then checking only a narrow strip of width 2δ around the midline. The critical insight: within that strip, sorting by y-coordinate and comparing each point to its next 7 neighbors guarantees correctness without blowing up to O(n²).
In practice, the strip is where most implementations silently regress to O(n²). The strip can contain O(n) points in the worst case (e.g., all points lie near the midline). Without the y-sorted neighbor limit, you'll compare every point in the strip against every other — exactly the naive algorithm again. The 7-point bound is tight: it relies on the fact that points within a δ×2δ rectangle cannot be too dense without violating the minimum distance from each half.
Use this algorithm when you need to compute proximity in spatial data — collision detection, GIS nearest-neighbor queries, or clustering initialization. It's the foundation for higher-dimensional variants (e.g., closest pair in 3D) and for approximate nearest-neighbor search. In real systems, the O(n log n) version is mandatory for n > 10,000; below that, the constant factors of recursion and sorting often make the naive O(n²) faster.
⚠ The Strip Is Not Free
If you skip sorting the strip by y-coordinate before scanning, you lose the 7-point guarantee and your algorithm silently becomes O(n²) — even with the divide-and-conquer structure.
📊 Production Insight
Teams using this for real-time collision detection in a game engine hit frame drops when thousands of objects cluster near a boundary line — the strip explodes and the O(n log n) promise breaks.
The symptom: CPU profiling shows the strip-check function consuming 80%+ of the frame time, despite the rest of the algorithm running in milliseconds.
Rule of thumb: always pre-sort points by y once and pass the sorted list through recursion — re-sorting per level adds a hidden O(n log² n) factor that kills performance at scale.
🎯 Key Takeaway
Divide-and-conquer gives O(n log n) only if you enforce the 7-point neighbor limit in the strip — otherwise it's O(n²).
Always pre-sort by y-coordinate once and reuse the sorted order across recursive calls to avoid O(n log² n) blowup.
The naive O(n²) is faster for n < 10,000 due to lower constant factors — don't over-engineer small inputs.
thecodeforge.io
Closest Pair Of Points
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
closest_pair.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# Package: io.thecodeforge.closest_pairfrom math import dist
from itertools import combinations
defclosest_pair(points: list[tuple]) -> tuple[float, tuple, tuple]:
points = sorted(points, key=lambda p: p[0])
return_closest(points)
def_closest(pts):
n = len(pts)
if n <= 3:
# Brute force for small input
best = float('inf'), pts[0], pts[1]
for a, b incombinations(pts, 2):
d = dist(a, b)
if d < best[0]: best = d, a, b
return best
mid = n // 2
mid_x = pts[mid][0]
d_left = _closest(pts[:mid])
d_right = _closest(pts[mid:])
best = d_left if d_left[0] < d_right[0] else d_right
delta = best[0]
# Build strip: points within delta of dividing line
strip = [p for p in pts ifabs(p[0] - mid_x) < delta]
strip.sort(key=lambda p: p[1]) # sort by y# Check strip pairs (at most 7 comparisons per point)for i inrange(len(strip)):
for j inrange(i+1, min(i+8, len(strip))):
if strip[j][1] - strip[i][1] >= delta:
break
d = dist(strip[i], strip[j])
if d < delta:
best = d, strip[i], strip[j]
delta = d
return best
points = [(2,3),(12,30),(40,50),(5,1),(12,10),(3,4)]
d, p1, p2 = closest_pair(points)
print(f'Closest: {p1} and {p2}, distance: {d:.4f}')
Output
Closest: (2, 3) and (3, 4), distance: 1.4142
📊 Production Insight
Skipping the y-sort on the strip is the most common production mistake.
It turns the O(n) combine into an O(n²) disaster for dense datasets.
Rule: always sort strip by y before the inner loop — profile to confirm linear time.
🎯 Key Takeaway
Divide by x, recurse, then combine in O(n) by checking only 7 neighbours per point in a sorted strip.
That's the recipe that gives O(n log n).
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.
🔥Complexity
T(n) = 2T(n/2) + O(n log n) [sorting strip] or O(n) [if strip pre-sorted].
With pre-sorting by y: T(n) = 2T(n/2) + O(n) → O(n log n) total by Master Theorem Case 2.
📊 Production Insight
The proof only works if the strip is sorted by y.
Without that, you can't break the inner loop early, and the bound becomes O(n²).
Rule: the geometric bound depends on the ordering — enforce it in code.
🎯 Key Takeaway
Maximum 8 points fit in a 2δ×δ rectangle with pairwise distance ≥ δ.
Therefore, each point checks ≤ 7 neighbours — the combine step is O(n).
thecodeforge.io
Closest Pair Of Points
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.
📊 Production Insight
Pre-sorting trades memory for time. For memory-constrained environments (e.g., embedded systems), re-sorting the strip may be acceptable if n is small.
But for production at scale, the extra O(log n) factor becomes noticeable at millions of points.
Rule: always pre-sort by y when n > 10⁵ — the memory overhead is linear.
🎯 Key Takeaway
Maintain a separate y-sorted list to avoid O(n log n) per combine.
This reduces recurrence to T(n) = 2T(n/2) + O(n) = O(n log n).
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).
Let's walk the math
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.
📊 Production Insight
Engineers often claim O(n log n) without pre-sorting, but the correct bound is O(n log² n).
For n=10⁶, log² n ≈ 400 vs log n ≈ 20 — that's 20x slower.
Rule: always state the recurrence honestly and include pre-sorting in your implementation.
🎯 Key Takeaway
Without pre-sort: O(n log² n). With pre-sort: O(n log n).
The difference matters at scale — pre-sort is not a micro-optimisation.
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.
closest_pair_optimized.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# Package: io.thecodeforge.closest_pair_optimizedfrom math import dist
defclosest_pair_with_pre_sort(points: list[tuple]) -> tuple[float, tuple, tuple]:
# Pre-sort by x for divide step
px = sorted(points, key=lambda p: p[0])
py = sorted(points, key=lambda p: p[1])
return_closest(px, py)
def_closest(px: list, py: list) -> tuple[float, tuple, tuple]:
n = len(px)
if n <= 3:
# Brute force
best = float('inf'), px[0], px[1]
for i inrange(n):
for j inrange(i+1, n):
d = dist(px[i], px[j])
if d < best[0]:
best = d, px[i], px[j]
return best
mid = n // 2
mid_x = px[mid][0]
# Split y-sorted points into left and right based on x
py_left = []
py_right = []
for p in py:
if p[0] <= mid_x andlen(py_left) < mid:
py_left.append(p)
else:
py_right.append(p)
# Recurse
d_left = _closest(px[:mid], py_left)
d_right = _closest(px[mid:], py_right)
delta = min(d_left[0], d_right[0])
# Build strip from py (already sorted by y)
strip = [p for p in py ifabs(p[0] - mid_x) < delta]
# Check up to 7 neighbours
best = d_left if d_left[0] < d_right[0] else d_right
for i inrange(len(strip)):
for j inrange(i+1, min(i+8, len(strip))):
if strip[j][1] - strip[i][1] >= delta:
break
d = dist(strip[i], strip[j])
if d < best[0]:
best = d, strip[i], strip[j]
delta = d
return best
Output
Same as above but handles pre-sorting.
📊 Production Insight
Duplicate points cause δ=0, which empties the strip and misses cross-half duplicates.
In production geospatial data, duplicates happen more often than you think.
Rule: deduplicate points before running the algorithm, or add a zero-distance check at the start.
🎯 Key Takeaway
Handle base case n≤3 by brute force. Handle duplicates explicitly.
Pre-sort by y to keep combine linear. Use squared distances for integers.
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.
📊 Production Insight
Extending to 3D naively increases the constant from 7 to ~32. Still O(n log n) but 4x slower per point.
For dimensions > 3, the constant factor makes D&C slower than brute force for reasonable n.
Rule: choose the algorithm based on dimensionality, not just asymptotic complexity.
🎯 Key Takeaway
Works for any fixed dimension, but constant grows exponentially.
For 3D, neighbour bound is ~31. For > 3D, use approximate methods.
The Naive Approach: Why You Still Need to Know O(n²)
Before you dismiss the brute-force solution, understand this: it's the baseline every optimization must beat. It's also the only correct answer in interviews when the divide-and-conquer implementation has a bug you can't fix in 30 minutes.
The naive approach is trivial — compute Euclidean distance for every unordered pair, track the minimum. O(n²) time, O(1) space. For n=1000 that's ~500k distance calculations. For n=10⁵ it's catastrophic (~5 billion). But here's the kicker: it's the simplest correct implementation. When you're debugging the divide-and-conquer version and the strip merge logic is melting your brain, being able to fall back to the brute-force version to verify outputs isn't weakness — it's discipline.
I've seen production systems ship with brute-force because the constraints were small enough, and the team didn't trust the D&C implementation edge cases. Know your limits. Use brute-force as the reference implementation, not the final answer.
ClosestPairBruteForce.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// io.thecodeforge — dsa tutorialpublicclassClosestPairBruteForce {
publicstaticdoublebruteForceClosest(Point[] points) {
double minDist = Double.MAX_VALUE;
int n = points.length;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
double dist = euclidean(points[i], points[j]);
if (dist < minDist) {
minDist = dist;
}
}
}
return minDist;
}
privatestaticdoubleeuclidean(Point a, Point b) {
double dx = a.x - b.x;
double dy = a.y - b.y;
returnMath.sqrt(dx * dx + dy * dy);
}
staticclassPoint {
double x, y;
Point(double x, double y) { this.x = x; this.y = y; }
}
}
Output
closest distance: 1.4142135623730951
(number of pairs checked: 6 for 4 points)
⚠ Production Trap:
Never use brute-force on unsorted input in production unless you have proven n < 2000 with strict latency bounds. The O(n log n) variant isn't just faster — it's safer on large datasets.
🎯 Key Takeaway
The brute-force is your reference implementation. Write it first. Debug the optimized version against it.
The Divide-and-Conquer Expected Approach: Where the Real Money Is
The O(n log n) solution is the one that makes this problem famous. Here's the skeleton: sort points by x, recursively find min distance in left and right halves, then handle the strip where points straddle the dividing line.
The critical insight that most tutorials gloss over: the strip check is not O(n²) because you can prove only 7 points can exist in any δ × 2δ rectangle. That's the 7-neighbour theorem. After sorting strip points by y, for each point you only check the next 7. This keeps the merge step O(n) and the recurrence T(n) = 2T(n/2) + O(n), giving O(n log n).
Where implementations almost always fail: floating-point comparison for equality in the strip boundary, and off-by-one errors in the recursive calls. Use a tolerance, not ==. And for god's sake, pre-sort the points by y once and pass around index arrays — sorting the strip fresh each recursion kills your complexity.
Pre-sort by y globally, not per recursion level. Pass indices into the global y-sorted array. Most implementations waste O(n log² n) by re-sorting.
🎯 Key Takeaway
The O(n log n) solution is the industry standard. Master the strip merge — that's where 90% of bugs live.
Getting Started With Python’s min() and max() Functions
Before diving into the closest pair of points algorithm, you need a firm grasp of Python's min() and max()functions. These built-in functions are deceptively simple yet powerful tools for data extraction and comparison. At their core, they return the smallest or largest item in an iterable or among multiple arguments. Understanding their behavior prevents subtle bugs when processing point coordinates or distances during divide-and-conquer steps. The functions work by comparing elements using the less-than operator by default, but this can be customized. Knowing how to call these functions correctly with a single iterable versus multiple arguments is essential for clean code. For example, when you have a list of points as tuples, min(points) compares the first element of each tuple unless you specify a key. This foundational skill will help you implement efficient distance comparisons in the closest pair algorithm without reinventing the wheel.
MinMaxDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
// io.thecodeforge — dsa tutorialimport java.util.*;
publicclassMinMaxDemo {
publicstaticvoidmain(String[] args) {
int[] arr = {3, 1, 4, 1, 5, 9};
int min = Arrays.stream(arr).min().getAsInt();
int max = Arrays.stream(arr).max().getAsInt();
System.out.println("Min: " + min + ", Max: " + max);
// Output: Min: 1, Max: 9
}
}
Output
Min: 1, Max: 9
⚠ Production Trap:
In Python, min() and max() on an empty iterable raise ValueError unless you provide a default. Always check for empty data when processing coordinate lists.
🎯 Key Takeaway
Use min() and max() with a default parameter to avoid crashes on empty collections.
Processing Dictionaries With min() and max()
When working with dictionaries in the closest pair problem, you often need to find the point with minimum or maximum distance or coordinate value. Python's min() and max() on a dictionary by default operate on keys, not values. This common pitfall leads to incorrect results when you intend to compare distances or y-coordinates stored as dictionary values. To correctly find the key associated with the minimum or maximum value, use the key parameter: min(dict, key=dict.get). This returns the key whose value is smallest. For example, if you map point IDs to their y-coordinates, min(point_map, key=point_map.get) gives you the ID of the point with the smallest y. In a divide-and-conquer context, this technique is invaluable for selecting candidate points from a strip sorted by y. Understanding this subtlety prevents off-by-one errors and ensures your algorithm compares the right attributes. Always verify whether you need keys or values before calling these functions.
DictMinMax.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
// io.thecodeforge — dsa tutorialimport java.util.*;
publicclassDictMinMax {
publicstaticvoidmain(String[] args) {
Map<String, Integer> map = newHashMap<>();
map.put("A", 10); map.put("B", 3); map.put("C", 7);
// Find key with minimum value using StreamsString minKey = map.entrySet().stream()
.min(Map.Entry.comparingByValue())
.get().getKey();
System.out.println("Min value key: " + minKey); // B
}
}
Output
Min value key: B
⚠ Production Trap:
Calling min() directly on a dictionary returns the smallest key lexicographically, not the key with the smallest value — a silent logic error.
🎯 Key Takeaway
Use dict.get as the key argument to find the key with the minimum or maximum value.
thecodeforge.io
Closest Pair Of Points
Tweaking the Standard Behavior of min() and max() With key and default
In the closest pair of points algorithm, you'll perform many comparisons that depend on custom criteria — like distance between points rather than raw values. Python's min() and max() accept a key parameter that transforms each element before comparison, without modifying the original data. For instance, to find the point pair with smallest Euclidean distance, use key=lambda pair: dist(pair[0], pair[1]). This avoids writing explicit loops. Additionally, the default parameter prevents errors when the iterable is empty, returning a fallback value instead of raising ValueError. This is critical when processing an empty list of candidate pairs in the divide step. Combining key with default makes your code robust and concise. For example, min(pair_list, key=distance_func, default=None) safely returns None if no pairs exist. Mastering these tweaks transforms min() and max() from simple utilities into powerful algorithmic tools, saving development time and reducing bugs in production code.
Providing a key that returns a non-comparable type (like a list) will raise TypeError; always ensure the key function returns a comparable value.
🎯 Key Takeaway
The key parameter allows custom comparison logic; default provides a safe fallback for empty inputs.
● Production incidentPOST-MORTEMseverity: high
Closest Pair Blowup in Geospatial Query
Symptom
The closest pair endpoint returned correct results but took over 30 seconds for 10K points, while the team expected sub-second latency.
Assumption
The team assumed the 7-neighbour bound alone guaranteed O(n) combine, regardless of how the strip was processed.
Root cause
The strip was not sorted by y-coordinate. The inner loop compared each point against every other point in the strip, not just the nearest 7, leading to O(n²) behaviour for dense datasets.
Fix
Added 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.
Key lesson
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.
Production debug guideSymptom-driven actions for production and implementation issues3 entries
Symptom · 01
Algorithm takes more than O(n log n) time on moderate n
→
Fix
Check if the strip list is sorted by y before the inner loop. Add a runtime assertion that the strip's y-coordinates are non-decreasing. If absent, sort the strip.
Symptom · 02
Answer is incorrect or misses closest pair across dividing line
→
Fix
Verify that δ is correctly updated after every closer pair found within the strip. Also ensure the strip includes points exactly at distance δ from the midline (use < δ, not <=).
Symptom · 03
Stack overflow on recursive calls
→
Fix
Check the base case: for n ≤ 3, use brute force. If recursion goes deeper than log₂(n), the split may be uneven. Ensure sorting by x is stable and the median index is correct.
Closest Pair Approaches Comparison
Algorithm
Time Complexity
Space Complexity
When to Use
Naive (Brute Force)
O(n²)
O(1)
n ≤ 1000, prototyping
Divide and Conquer (Pre-sorted)
O(n log n)
O(n)
General purpose, n up to 10⁷
Shamos-Hoey (Sweep Line)
O(n log n)
O(n)
When points are streaming or you need incremental updates
⚙ Quick Reference
7 commands from this guide
File
Command / Code
Purpose
closest_pair.py
from math import dist
Algorithm Overview
closest_pair_optimized.py
from math import dist
Implementation Details and Edge Cases
ClosestPairBruteForce.java
public class ClosestPairBruteForce {
The Naive Approach
ClosestPairDivideConquer.java
public class ClosestPairDivideConquer {
The Divide-and-Conquer Expected Approach
MinMaxDemo.java
public class MinMaxDemo {
Getting Started With Python’s min() and max() Functions
DictMinMax.java
public class DictMinMax {
Processing Dictionaries With min() and max()
KeyDefaultDemo.java
public class KeyDefaultDemo {
Tweaking the Standard Behavior of min() and max() With key a
Key takeaways
1
Sort by x, divide at median, recurse on each half to get δ_L and δ_R. δ = min(δ_L, δ_R). Check strip of width 2δ around the dividing line.
2
The 7-point bound
in a 2δ×δ rectangle, at most 8 points can be placed with mutual distance ≥ δ (2×2 arrangement per half). So each point checks ≤ 7 neighbours in the strip.
3
Strip must be sorted by y-coordinate for the 7-point bound to apply. Pre-sorting by y (merging during the divide step) achieves T(n) = 2T(n/2) + O(n) → O(n log n).
4
O(n log n) is optimal for comparison-based closest pair
same lower bound as sorting.
5
The packing argument generalises
closest pair in higher dimensions also bounds strip neighbours by a constant, though the constant grows with dimension.
6
Production failure
missing y-sort turns O(n log n) into O(n²). Always sort your strip.
Common mistakes to avoid
5 patterns
×
Forgetting to sort the strip by y-coordinate
Symptom
Algorithm runs in O(n²) for dense datasets; profiling shows the inner loop executing far more than 7 comparisons per point.
Fix
Add 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
Symptom
Misses the closest pair when it lies exactly on the dividing line; algorithm returns a slightly larger distance than correct.
Fix
Use < 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
Symptom
Algorithm returns distance > 0 when duplicate coordinates exist, or misses the true minimum distance of 0.
Fix
Preprocess to deduplicate points using a set, or check for zero distance in the base case and return immediately.
×
Base case too small (n ≤ 1) or too large (n ≤ 10)
Symptom
Recursion error or performance hit. n ≤ 1 fails because two points are required; n > 3 adds unnecessary recursion overhead.
Fix
Set base case to n ≤ 3. Use brute force with O(n²) for those small subsets.
×
Not updating delta after finding a closer pair in the strip
Symptom
Correct distance found but later comparisons use the old delta, possibly missing an even closer pair within the same strip.
Fix
Update delta after each closer pair found: if d < delta: best = ...; delta = d. Then the inner loop's break condition uses the updated delta.
Why does each point in the strip need at most 7 comparisons?
Q02SENIOR
What is the recurrence and complexity of the closest pair algorithm with...
Q03JUNIOR
What happens at the base case and why is brute force acceptable there?
Q04SENIOR
How would you extend this to 3D?
Q05SENIOR
How can you optimise the closest pair algorithm for floating-point coord...
Q01 of 05SENIOR
Why does each point in the strip need at most 7 comparisons?
ANSWER
Consider a 2δ×δ rectangle centred on the reference point, extending δ left and right of the midline. The rectangle is split into two δ×δ squares. Within each square, the closest pair distance is at least δ (from the recursive result). In a δ×δ square, the maximum number of points with pairwise distance ≥ δ is 4 (arranged in a 2×2 grid). So the whole rectangle contains at most 8 points (including the reference), meaning at most 7 others to check. This packing argument holds only if the strip is sorted by y.
Q02 of 05SENIOR
What is the recurrence and complexity of the closest pair algorithm with and without pre-sorting by y?
ANSWER
Without pre-sorting: T(n) = 2T(n/2) + O(n log n) → O(n log² n) by Master Theorem. With pre-sorting (merging y-sorted lists): T(n) = 2T(n/2) + O(n) → O(n log n). The pre-sorting eliminates the log factor from the combine step.
Q03 of 05JUNIOR
What happens at the base case and why is brute force acceptable there?
ANSWER
For n ≤ 3, brute force is used because the number of pairs is tiny (≤ 3). Recursion overhead exceeds the O(1) brute force cost. It also avoids the complexity of handling small subsets with the geometric bound.
Q04 of 05SENIOR
How would you extend this to 3D?
ANSWER
The same D&C pattern works: sort by x, find δ recursively, then check a 3D slab of width 2δ. The packing argument becomes a 2δ×δ×δ box divided into 8 cubes. The maximum number of points with mutual distance ≥ δ in a δ×δ×δ cube is 8 (a 2×2×2 grid). So the slab contains at most 8×8 = 64 points, giving a neighbour bound of 63. The algorithm remains O(n log n) but with a much larger constant. For production, you'd pre-sort by y and z.
Q05 of 05SENIOR
How can you optimise the closest pair algorithm for floating-point coordinates?
ANSWER
Use squared distances to avoid the sqrt operation in every comparison. Only take the sqrt once at the end. This significantly reduces floating-point overhead. Also, careful with epsilon comparisons: use < epsilon for equality to avoid precision issues, but for the strip width, use < delta (not ≤) to stay true to the geometric bound. Pre-sorting by y remains crucial.
01
Why does each point in the strip need at most 7 comparisons?
SENIOR
02
What is the recurrence and complexity of the closest pair algorithm with and without pre-sorting by y?
SENIOR
03
What happens at the base case and why is brute force acceptable there?
JUNIOR
04
How would you extend this to 3D?
SENIOR
05
How can you optimise the closest pair algorithm for floating-point coordinates?
SENIOR
FAQ · 4 QUESTIONS
Frequently Asked Questions
01
Is O(n log n) optimal for closest pair?
Yes — any comparison-based algorithm requires Ω(n log n) by reduction from element distinctness. The D&C algorithm is therefore asymptotically optimal.
Was this helpful?
02
Why 7 and not 8?
The reference point itself is one of the up-to-8 points in the rectangle, so it checks at most 7 others.
Was this helpful?
03
What if points have the same x-coordinate?
The median is chosen after sorting by x. If many points share the median x, the split may be uneven, but the algorithm still works. The strip includes points on both sides of the dividing line, so vertical lines are handled.
Was this helpful?
04
Can I use this algorithm for large-scale geospatial queries?
Yes, but pre-process to handle duplicates and watch out for floating-point precision. For millions of points, pre-sorting by y is essential. Also consider using a sweep-line variant if the points are indexed in a spatial database.