KD Tree

A KD-tree (k-dimensional tree) is a binary tree that partitions k-dimensional space by alternating splitting axes. It enables fast nearest-neighbor and range queries in low dimensions — the workhorse of scipy.spatial.cKDTree and many computer-graphics systems.

Complexity

Average Worst Space
AccessSearchInsertionDeletion AccessSearchInsertionDeletion Worst
Θ(log n) Θ(log n) Θ(log n) Θ(log n) O(n) O(n) O(n) O(n) O(n)

How it works

At depth d, split by axis d mod k. Build: pick the median along that axis as the root, recurse on each half. Nearest-neighbor query: descend to the leaf containing the target; on the way back up, check whether the bounding hyperplane could contain a closer point and recurse into the other side if so. O(log n) average for low k; degrades sharply when k > ~20 (curse of dimensionality).

Python implementation

class Node:
    __slots__ = ("point", "axis", "left", "right")
    def __init__(self, point, axis):
        self.point, self.axis = point, axis
        self.left = self.right = None


def build(points, depth=0):
    if not points: return None
    k = len(points[0])
    axis = depth % k
    points.sort(key=lambda p: p[axis])
    mid = len(points) // 2
    n = Node(points[mid], axis)
    n.left = build(points[:mid], depth + 1)
    n.right = build(points[mid + 1:], depth + 1)
    return n


def _dist2(a, b):
    return sum((x - y) ** 2 for x, y in zip(a, b))


def nearest(node, target, best=None):
    if node is None: return best
    d = _dist2(node.point, target)
    if best is None or d < best[1]:
        best = (node.point, d)
    diff = target[node.axis] - node.point[node.axis]
    near, far = (node.left, node.right) if diff < 0 else (node.right, node.left)
    best = nearest(near, target, best)
    if diff * diff < best[1]:
        best = nearest(far, target, best)
    return best


pts = [(2, 3), (5, 4), (9, 6), (4, 7), (8, 1), (7, 2)]
tree = build(pts)
print(nearest(tree, (9, 2)))   # ((8, 1), 2)

Trade-offs

Step-by-Step Build and Query Trace

KD-Tree Built from the Six Example Points (7, 2) split x = 7 (5, 4) split y = 4 (9, 6) split y = 6 (2, 3) (4, 7) (8, 1) nearest to (9, 2) Depth 0 splits on x (median 7), depth 1 on y, depth 2 would split on x again. Query (9, 2): go right at (7, 2), left at (9, 6), reach (8, 1) with distance squared 2.

Building the tree for the points (2, 3), (5, 4), (9, 6), (4, 7), (8, 1), (7, 2) starts by sorting on x and taking the median, (7, 2), as the root. Points with smaller x go left, larger x go right. At depth 1 each half is sorted on y: the left half yields (5, 4) with children (2, 3) and (4, 7), the right half yields (9, 6) with child (8, 1). For the query (9, 2), the search compares x = 9 with the root's 7 and goes right, compares y = 2 with 6 and goes left, and reaches (8, 1) at squared distance 2. Unwinding, it checks whether the splitting planes are closer than the best distance found: the plane y = 6 is 16 units squared away and the plane x = 7 is 4 units squared away, both farther than 2, so the entire left subtree and the other branches are pruned.

Nearest-Neighbour Search

Nearest-Neighbour Search with Pruning Descendnear side first Update bestif closer Check planediff² < best? Visit far sideonly if needed Return bestpoint, dist² The far subtree is skipped when the splitting plane is farther away than the current best distance, which is what makes the search fast.

Each node represents an axis-aligned region of space. The search first walks to the leaf region that contains the query, because the nearest point is likely nearby, which quickly gives a tight best distance. While unwinding the recursion, it compares the squared distance from the query to the splitting hyperplane (diff * diff) with the best squared distance. If the plane is farther than the best point, no point on the other side can be closer and that subtree is skipped. With well-spread data in low dimensions this prunes most of the tree and visits O(log n) nodes on average.

The same structure supports range search (report all points inside a box) and k-nearest-neighbour search by keeping a bounded max-heap of the k best candidates instead of a single best.

Using SciPy in Practice

import numpy as np
from scipy.spatial import cKDTree

rng = np.random.default_rng(0)
pts = rng.random((100_000, 3))           # 100k points in 3-D
tree = cKDTree(pts, leafsize=16)

dist, idx = tree.query([0.5, 0.5, 0.5], k=5)       # 5 nearest
near = tree.query_ball_point([0.5, 0.5, 0.5], r=0.02)  # radius search
pairs = tree.query_pairs(r=0.005)                   # all close pairs
print(idx, len(near), len(pairs))

Production implementations store several points per leaf (leafsize) and use iterative traversal and vectorised distance computations; a hand-written recursive tree in Python is useful for learning but far slower. scikit-learn's KNeighborsClassifier chooses between brute force, KD-tree and ball tree automatically with algorithm="auto".

When to Use It

MethodGood forDimensionsExact?
KD-treeNN and range queries on static pointslow (roughly up to 10 to 20)yes
Ball treenon-Euclidean metrics, clustered datamoderateyes
Brute force (vectorised)small n or very high danyyes
HNSW / IVF (faiss, vector DBs)embeddings, millions of vectorshigh (hundreds+)approximate
R-treerectangles and geographic shapes2 to 3yes

KD-trees are ideal for geometric problems in 2 or 3 dimensions: GIS points, collision detection, point clouds, colour quantisation. For text or image embeddings with hundreds of dimensions, the pruning test almost never succeeds, so the search visits nearly every node; approximate nearest-neighbour indexes are the right tool there.

Pitfalls

← Back to Algorithms