Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Balanced Trees  ›  Lesson

k-d Trees & Nearest Neighbour

Spatial Trees 28 minSplitting the plane by alternating x/y turns nearest-neighbour search into pruning whole half-planes instead of checking every point
You're building a piece ofQuarry — the index layer
This piece — nearest_neighbor(): Finds the closest point on a map by pruning whole regions of the plane.
Scenario Quarry's index layer needs to answer "what's the closest stop to here?" against a real map, without scanning every point for every query.
Your task
Build nearest_neighbor(points, query): build a k-d tree from `points` (a list of [x, y] or (x, y) pairs), splitting on x at depth 0, y at depth 1, x at depth 2, and so on, always on the MEDIAN of the points at that level. Then search it for the point nearest to `query`, pruning a branch whenever the splitting plane alone is already farther than the best distance found so far. Return {"nearest": [x, y] or None, "distance": rounded to 4 decimals or None, "visited": <nodes visited>, "brute_force_visited": <len(points)>}. Example: nearest_neighbor([[2,3],[5,4],[9,6],[4,7],[8,1],[7,2]], [9,2]) -> {"nearest": [8,1], "distance": 1.4142, "visited": 3, "brute_force_visited": 6}.

k-d Trees & Nearest Neighbour

In short: every balanced tree so far split on one key. A k-d tree splits a set of points on alternating coordinates — x at the root, y at its children, x again below that — always at the median, so each split keeps the two halves even. The payoff is nearest-neighbour search: instead of checking every point, you can prune a whole half-plane the moment its splitting line alone is already farther away than the best point found so far. No distance check is needed inside a pruned half — it's ruled out by one comparison, not by visiting it.

A square divided by a vertical cut line, each half divided again by a horizontal cut line, with a handful of dots inside the small regions and one dot circled as the answer to a nearest-point query, with an arrow showing one shaded region ruled out without being checked

Snapping a map tap to the nearest pin

A maps app has a few thousand saved pins and needs to answer "which pin did the user just tap near?" many times a second while they drag the map. A k-d tree built once over the pins answers each query by pruning, not scanning:

python

The nearest pin to (9, 2) is (8, 1) — found after visiting only 3 of the 6 pins. The other 3, all on the far side of the root's split line, were ruled out in one comparison each.

Pruning one half-plane

watch it move · tree

The k-d tree over 6 pins, alternating x-splits (root, and its grandchildren) and y-splits (the middle level). Searching for the pin nearest (9, 2).

Every node is badged by which coordinate it split on when it was built — x at the root and its grandchildren, y one level down. Binary tree, 6 nodes, height 3.

1 / 7

A sensor grid that only ever splits one way

A warehouse's floor sensors are logged as (x, y) pairs and a robot needs "closest working sensor to here." The mistake people actually make building this: hard-coding the split to always use x, forgetting to alternate with depth.

What does this print?
def axis_for_depth(depth):
    return depth % 2    # 0 = x, 1 = y

print(axis_for_depth(0), axis_for_depth(1), axis_for_depth(2), axis_for_depth(3))

If every split used axis = 0 instead of depth % 2, points that share similar x-coordinates but spread widely in y would still end up on one lopsided branch — the tree degrades toward a 1-D structure built on a 2-D problem, and the height guarantee from alternating axes disappears.

broken — fix it

This prints [0] (only ever splits on x) instead of [0, 1]. Fix `axis` to depend on `depth`.

What it costs

operation cost why
build O(n log n) sorting at each of O(log n) levels, O(n) total per level
nearest-neighbour search, balanced data O(log n) expected each level prunes roughly half the remaining points
nearest-neighbour search, worst case O(n) a query that can't rule out any branch (e.g. all points on one line) still visits everything
space O(n) one node per point, no extra structure

Quick practice

What does this print?
def squared_dist(a, b):
    return (a[0]-b[0])**2 + (a[1]-b[1])**2

print(squared_dist((7,2), (9,2)) < squared_dist((8,1), (9,2)))
broken — fix it

This prints (9, 6), which is squared-distance 17 from (8, 10), instead of the true nearest point (10, 9), squared-distance 5. The near side alone isn't enough — add back the pruning check that decides whether to explore the far side too.

🎯 Your turn

Build nearest_neighbor(points, query): build a k-d tree from points (alternating x/y splits, always the median), then search it for the point nearest query, pruning a branch whenever the splitting plane alone is already farther than the best distance found so far. Return {"nearest": [x, y] or None, "distance": rounded to 4 decimals or None, "visited": ..., "brute_force_visited": len(points)}.

  • nearest_neighbor([[2,3],[5,4],[9,6],[4,7],[8,1],[7,2]], [9,2]) → {"nearest": [8,1], "distance": 1.4142, "visited": 3, "brute_force_visited": 6}

Hint: build once with the median-split recursion from this lesson, then search once: visit the near side first, and only visit the far side if diff * diff (the squared distance to the splitting plane) is still less than the best squared distance found so far.

Press ▶ Run, try the Live App chip, then ✓ Check.

Practice — level up

0/5 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upHow deep does it build?

Write kdtree_depth(points): build a k-d tree from points (alternating x/y splits, median each level) and return its height. Empty points has depth 0.

spatial-trees
DrillWhat's the root split on?

Write root_point(points): return the point (as a list) that becomes the k-d tree's root — the median point when points is sorted by x (axis 0, depth 0). Return None if points is empty.

spatial-trees
BuildThe baseline to beat

Write brute_force_nearest(points, query): check every point and return the one closest to query (as a list), by checking squared distance. Return None for empty points.

spatial-trees
BossHow much pruning actually saved

Write pruning_savings(points, query): run the lesson's nearest_neighbor(points, query) and return {"visited": ..., "brute_force": ..., "saved": ...} where saved is how many fewer nodes the k-d tree search visited than a brute-force scan would.

spatial-treescomplexity
CapstonePut it together

Write nearest_neighbor(points, query): the full lesson task — build a k-d tree (median split, alternating x/y), search it with pruning, and return {"nearest": [...] or None, "distance": rounded to 4 decimals or None, "visited": ..., "brute_force_visited": len(points)}.

spatial-treesbalanced-trees
nearest_neighbor.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
nearest_neighbor(points, query) → dictThe nearest point to query, its distance, and how many k-d tree nodes the pruned search visited versus a brute-force scan.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.