Spatial Data Structures
A balanced BST orders keys on a line, but points in the plane have no single natural order. Quadtrees subdivide space recursively into quadrants; k-d trees split on alternating coordinates at the median.
╌╌╌╌
A balanced search tree is built on one assumption: the keys are
totally ordered, so a single comparison sends a query left or right. Points
in the plane break that assumption. There is no order on that makes
both all points in this rectangle
and the point nearest to
cheap — sort by
and two points far apart in end up adjacent; sort by and the reverse.
A spatial data structure stores points so that geometry, not a one-dimensional
key, guides the search: it partitions space into boxes, and a query that lands
in one box can ignore every box it cannot possibly intersect.1
Two partitions dominate. A quadtree splits each square into four equal quadrants, recursively, until each cell is simple. A k-d tree splits on one coordinate at a time — , then , then again — always at the median, so the tree stays balanced. The quadtree is splitting space into a fixed grid; the k-d tree is splitting the points into equal halves. That difference is what makes one degrade on clustered data while the other does not.
Quadtrees: recursive subdivision of space
A point quadtree stores a set of 2-D points by recursively cutting an axis-aligned square into four equal sub-squares — the four quadrants , , , — until every cell holds at most one point. Each internal node owns a square region and has up to four children, one per quadrant; each leaf holds at most one point (or, in a bucket variant, up to points before it splits).
Insert. To insert a point , descend from the root: at each internal node decide which of the four quadrants contains (two coordinate comparisons against the node's centre) and recurse into that child. When you reach an empty leaf, store there. If you reach a leaf that already holds a point , split that leaf into four quadrants and re-insert both and into the appropriate sub-quadrants — repeating until they fall into different cells. Search for an exact point is the same descent without the split.
Region query. To report every stored point inside a query rectangle , descend from the root but prune: at a node owning square ,
- if is disjoint from , return nothing — that whole subtree is skipped;
- if lies entirely inside , report every point in the subtree;
- otherwise straddles the boundary of , so recurse into all four children.2
The pruning is what makes the query fast: a query touching a small corner of the plane visits only the handful of cells along that corner, not the whole tree.
That last sentence is also the quadtree's weakness. Because it always cuts a square exactly in half regardless of where the points lie, two points a distance apart force levels of subdivision before they separate — the tree depth depends on the coordinates, not just on . A tightly clustered set, or points near-collinear at fine scale, can drive the height far beyond , so quadtree operations are with no worst-case bound tied to alone. Splitting the points in half instead of the space avoids this; that is the k-d tree.
k-d trees: split on alternating coordinates at the median
A k-d tree (-dimensional tree; here ) is a binary tree in which each internal node splits the remaining points by a single coordinate, and the splitting coordinate cycles with depth: the root splits on , its children split on , the grandchildren on again, and so on. At each node the split is at the median value of the active coordinate, so each child receives half the points — which forces height no matter how the points are distributed.
Geometrically, each split is an axis-aligned line (a hyperplane in higher dimensions): a vertical cut at the root, horizontal cuts at the next level, vertical again below that. The cuts nest, carving the plane into rectangular cells — one per leaf — each containing a single point.
Build. Given all points, build top-down. At depth pick the active coordinate , find the median of the points by coordinate (linear time via quickselect, or by pre-sorting), store it at the node, and recurse on the two halves. The recurrence solves to , and because every split is at the median the resulting tree has height .
Range search. Reporting the points in a query rectangle works just like the quadtree's, pruning on the cell each node owns: skip a subtree whose region is disjoint from , report a subtree whose region lies wholly inside , and recurse otherwise. A range query touching output points costs in the plane — the comes from the cells the query boundary crosses.3
Nearest-neighbour search with branch-and-bound
The k-d tree's signature query is nearest neighbour: given a query
point , find the stored point closest to it. The naive scan is ; the k-d
tree does it in expected time on well-distributed data by
branch-and-bound — descend to the leaf belongs
in to get a first
candidate, then unwind, only entering the other side of a split if a closer
point could possibly hide there.
The pruning test is geometric. At a node splitting coordinate at value , the far subtree lies entirely on the far side of the line . The closest that any far-side point could be to is the perpendicular distance from to that line, . If that distance already exceeds the best distance found so far, no point over there can beat the current best — so we skip the entire far subtree. Otherwise the far side might hold something closer, and we recurse into it too.
- 1if then return
- 2if then
- 3this node beats the incumbent
- 4active split coordinate
- 5if then
- 6
- 7else
- 8
- 9descend the likely side first
- 10if thensplitting line within reach?
- 11only then check the far side
- 12return
Visiting the near side first is what makes the bound tight: it usually finds a good candidate immediately, so the test fails at most far-side nodes and prunes them away. The same branch-and-bound extends to nearest neighbours (keep a bounded max-heap of the best , prune against its worst distance) and to approximate nearest neighbour (multiply the bound by a slack factor to prune more aggressively).
A worked query. Take the five points , , , , built into a k-d tree with at the root (split on ), and in the left subtree (), and in the right (). Query .
- Root , split on . Distance ; set , . Since , the near side is the right subtree; descend there first.
- Right child , split on . Distance , so does not improve . Since , the near child is 's right subtree holding .
- , a leaf. Distance ; no improvement. Unwind to : its far subtree lies below , at perpendicular distance — pruned.
- Unwind to the root. Its far subtree is the left side, , at perpendicular distance — the circle crosses the splitting line, so the left side could hide something closer and must be searched. It holds (distance ) and (distance ), neither beating .
The answer is at distance . The search touched , and the left subtree; the bound at step 3 pruned 's lower half, which a linear scan would have to examine.
The pruning is only effective in low dimensions. As grows the perpendicular distance to any single splitting line shrinks relative to the typical point-to-point distance, so the bound rarely fires and the search degrades toward the scan — the curse of dimensionality. Beyond roughly a dozen dimensions, exact k-d nearest neighbour offers little over brute force, and practitioners switch to approximate methods. In two or three dimensions, though, the k-d tree is the standard answer.
Range trees: faster orthogonal range reporting
The k-d tree answers a rectangle query in , and the term hurts when the query is thin and reports few points. A range tree trades space for a sharper query bound: in the plane, at a cost of storage instead of . It is built by nesting one balanced search tree inside another.
A rectangle query runs in two stages. First, search the primary () tree for the split node where the paths to and diverge; the points with are precisely those in a set of canonical subtrees hanging off the two search paths. Rather than walk each subtree, consult its associated -tree and report the points whose falls in — a 1-D range query costing . Summed over the canonical subtrees, the query is .
Fractional cascading removes one log factor. If the associated structures are linked so that a position found in one -list transfers in to the next, the repeated -searches collapse to a single search plus hops, giving per query. The construction generalizes to dimensions at space and query time — the standard answer when range reporting must be fast and the points are static.
Interval trees: which intervals contain a point
The structures so far index points. A different question indexes intervals on the line: given intervals and a query value , report every interval that contains (a stabbing query). This shows up in scheduling (which reservations are live at time ), computational geometry (segment intersection), and genome analysis (which features span a locus).
A stabbing query for descends from the root. At a node with center , every interval crossing is a candidate:
- if , scan the node's list sorted by left endpoint from the smallest, reporting each interval with and stopping at the first that fails (all later ones start even further right), then recurse left;
- if , scan the list sorted by right endpoint from the largest, reporting each with , then recurse right;
- if , every stored interval at the node contains ; report them all.
Each node scan reports hits in time, and the descent visits nodes, so a stabbing query costs for reported intervals. Build is and space is . When the query is itself an interval and you want every stored interval overlapping it, the same tree works with an overlap test at each node. Skiena's segment-tree and interval-tree treatments cover the reporting variants in full.1
Choosing a spatial structure
These structures index geometry so that a query prunes what it cannot reach; they differ in what they cut and what they index.
- Quadtree. Splits space into a fixed quadrant grid. Simple, supports easy dynamic insert/delete, and natural for images and uniformly-spread data. But its depth tracks the coordinates of the points, so clustered or near-coincident data blows up the height — no guarantee.
- k-d tree. Splits the points at the median on alternating axes, forcing height regardless of distribution. Build is , range search , nearest neighbour expected in low dimensions. Less natural to update incrementally (medians shift), and it succumbs to the curse of dimensionality in high .
- Range tree. Nests a -tree inside an -tree to answer rectangle reporting queries in (or with fractional cascading), at space. Best when queries are frequent, thin, and the point set is static.
- Interval tree. Indexes intervals rather than points, answering stabbing
and overlap queries in with space — the tool when the
data are ranges and the question is
what covers
.
In short: the quadtree halves the space, the k-d tree halves the data. Use the quadtree when the points are well-spread and updates must be cheap; the k-d tree when a balance guarantee and fast nearest-neighbour queries matter in two or three dimensions; the range tree when range reporting on static points must be as fast as possible; and the interval tree when the objects themselves are intervals.
Spatial indexing at scale
The structures here are in-memory and low-dimensional; production spatial systems push past both limits.
R-trees and the database index. The R-tree (Guttman, 1984) is the spatial
analogue of the B-tree: each node stores
the bounding rectangles of its children and fans out wide so the tree is short and
disk-friendly. It is what backs SPATIAL INDEX in PostGIS, SQLite's R*Tree
module, and most GIS systems, indexing not just points but arbitrary shapes by
their bounding boxes. Its refinements, the R*-tree and bulk-loaded
STR-tree, tune how rectangles are grouped to minimize overlap, the spatial
equivalent of keeping a B-tree's nodes full.
Trading exactness in high dimensions. The k-d tree's curse of dimensionality is real: past roughly - dimensions, nearest-neighbour search degrades to scanning almost everything. The standard workaround is approximate nearest neighbour. Locality-sensitive hashing (Indyk and Motwani, 1998) hashes nearby points to the same bucket with high probability; graph-based indexes like HNSW (Malkov and Yashunin, 2016) navigate a small-world graph. Both are what vector databases use to search millions of embeddings in milliseconds.
Flattening space to one dimension. A different trick maps 2-D or 3-D points onto a single space-filling curve, Z-order (Morton) or Hilbert, so that points close in space are usually close along the curve. Then an ordinary 1-D B-tree index answers spatial range queries approximately, which is how many key-value stores add geospatial lookups without a dedicated spatial tree. In graphics, the same recursive-subdivision idea becomes the bounding volume hierarchy that makes ray tracing tractable.4
Takeaways
- Points in the plane have no single natural order, so we index them by partitioning space into boxes and pruning boxes a query cannot reach.
- A quadtree recursively splits each square into four equal quadrants until cells are simple. Insert descends and splits a full leaf; region query prunes subtrees disjoint from / contained in the query rectangle.
- The quadtree's depth follows the geometry of the data: sparse regions stay shallow, clustered points force deep subdivision — so it has no worst-case bound.
- A k-d tree splits the points on alternating coordinates at the median, forcing height . Build ; range search .
- Nearest neighbour uses branch-and-bound: descend to the candidate leaf, then enter the far side of a split only when the perpendicular distance to the splitting line is below the best distance found.
- k-d pruning fails in high dimensions (curse of dimensionality); both point structures work best in 2-D and 3-D.
- A range tree nests a -tree in an -tree for orthogonal range reporting in ( with fractional cascading), using space.
- An interval tree indexes intervals, not points, and answers stabbing queries
(
which intervals contain
) in with space.
Footnotes
- Skiena, §12.6 — Kd-Trees: alternating-axis median splits, range search, and nearest-neighbour via branch-and-bound, with the curse-of-dimensionality caveat. ↩ ↩2
- Erickson, Ch. — Data Structures: recursive space-partitioning trees and the geometry of pruning a query against a subtree's bounding box. ↩
- CLRS, Ch. 33 — Computational Geometry: axis-aligned subdivision and the recurrence behind balanced spatial partitions. ↩
- Guttman,
R-trees: a dynamic index structure for spatial searching
(1984); Indyk & Motwani,Approximate nearest neighbors
(LSH, 1998); Malkov & Yashunin,Efficient and robust approximate nearest neighbor search using HNSW graphs
(2016). ↩
╌╌ END ╌╌