1. Introduction & Motivation
Every structure covered earlier in this series answers a question about a single key: is this value present, what is the predecessor of this key, is this key smaller than that one. Many real problems ask a different kind of question entirely: how do you index data so you can efficiently query a range or region, not just a single point?
That single framing question splits into three related but distinct problems, each of which needs its own structure:
- 1D ranges of intervals: "does any stored interval overlap $[lo, hi]$?" — two meetings that share time, two genomic segments that overlap. This is the Interval Tree.
- 1D ranges of points: "which stored points fall between $lo$ and $hi$?" — a sorted set of single numbers, queried repeatedly for sub-ranges. This is the Range Tree.
- Multi-dimensional regions: "which points or objects lie in this 2D (or 3D) box, or which one is closest to here?" — this is the domain of the k-d Tree, Quadtree/Octree, and R-Tree.
These structures share a family resemblance: nearly all of them are trees that store extra summary information at each node (an augmented BST, or a spatial subdivision) so that an entire subtree can be included or excluded from the answer without visiting every element inside it. That pruning step is the idea to watch for in every section below.
2. Prerequisites
This lesson assumes comfort with basic binary search trees: node structure, recursive insert, and the BST ordering property. If augmented BSTs — trees where each node stores extra derived information alongside its key — are unfamiliar, review the node structure and insertion walkthrough in Part 10: BST & Balanced Trees before continuing; the Interval Tree and Range Tree below are both augmented BSTs built directly on that foundation.
One distinction is worth making explicit up front: the Fenwick Tree and Segment Tree also answer "range" queries, but over a single sorted array index — range-sum, range-min, range-max on positions $0 \ldots n-1$. The structures in this lesson instead answer overlap queries between intervals, and range/region queries over point coordinates that don't already live at fixed array positions — a different problem shape, even though the word "range" shows up in both.
3. Interval Tree
max_end: the largest high endpoint anywhere in its subtree. During an overlap query, if a subtree's max_end is smaller than the query's low endpoint, nothing in that entire subtree can possibly overlap — so it is pruned without being visited.
Two intervals $[a, b]$ and $[c, d]$ overlap exactly when $a \le d$ and $c \le b$. The search walks down from the root: at each node, it checks whether the node's own interval overlaps the query; if not, it uses max_end to decide whether the left subtree could possibly contain an overlap, and only descends into the right subtree otherwise.
Hand-trace an overlap query
Insert, in order, the intervals (15,20), (10,30), (17,19), (5,8), (12,15), (30,40) keyed by low endpoint. The resulting tree (with each node's max_end):
| Node | Interval | Parent | max_end |
|---|---|---|---|
| root | (15,20) | — | 40 |
| left of root | (10,30) | (15,20) | 30 |
| left of (10,30) | (5,8) | (10,30) | 8 |
| right of (10,30) | (12,15) | (10,30) | 15 |
| right of root | (17,19) | (15,20) | 40 |
| right of (17,19) | (30,40) | (17,19) | 40 |
Query overlap of [22, 25]: at root (15,20), $15 \le 25$ but $22 \le 20$ is false, so no overlap here. Left child's max_end is 30, and $30 \ge 22$, so descend left to (10,30). There, $10 \le 25$ and $22 \le 30$ — overlap found. The query never visits (5,8), (12,15), (17,19), or (30,40): four of six nodes were pruned by a single max_end comparison.
4. Range Tree (1D)
A balanced BST over sorted points
A 1D Range Tree is a perfectly balanced BST built from a sorted set of points, where the median point becomes the root and the two halves recurse into the left and right subtrees. A range query $[lo, hi]$ first walks down from the root to find the split node — the last node where the search paths toward $lo$ and toward $hi$ still agree. Below the split node, the path toward $lo$ peels left and the path toward $hi$ peels right; every subtree hanging off the "inside" of each path is a canonical subtree that is entirely inside $[lo, hi]$, so it can be reported wholesale without checking each of its points individually. This gives a query time of $O(\log n + k)$, where $n$ is the number of stored points and $k$ is the number of points actually returned.
A 2D Range Tree extends this idea by attaching, to every node of the primary tree (built on the x-coordinate), a secondary Range Tree over the y-coordinates of that node's subtree — so an x-range query first finds $O(\log n)$ canonical subtrees, and then a y-range query runs against each of their secondary trees. That layered construction is only sketched here conceptually and is not implemented below; for this lesson's purposes, the k-d Tree in the next section solves the 2D range problem more directly and with a single, more intuitive tree.
5. k-d Tree
Hand-trace building a k-d tree
Build from the points (3,6), (17,15), (13,15), (6,12), (9,1), (2,7), (10,19), choosing the median of the current axis at each level so the tree stays balanced:
| Depth | Axis | Points considered (sorted) | Median chosen |
|---|---|---|---|
| 0 | x | (2,7),(3,6),(6,12),(9,1),(10,19),(13,15),(17,15) | (9,1) — root |
| 1 | y | left half by y: (3,6),(2,7),(6,12) | (2,7) — root's left child |
| 1 | y | right half by y: (13,15),(17,15),(10,19) | (17,15) — root's right child |
| 2 | x | single points remain | (3,6) and (6,12) as leaves under (2,7) |
| 2 | y | single points remain | (13,15) and (10,19) as leaves under (17,15) |
The finished tree splits on x at the root, y one level down, x again (not used further here since each subtree bottoms out at depth 2). Notice the axis alternates strictly by depth, never by which coordinate happens to vary more — that's what distinguishes it from a Quadtree, which always splits both axes together at a fixed spatial midpoint.
Nearest-neighbor search descends toward the query point like a normal BST search, recording the closest point seen so far. On the way back up, it only checks the other child's subtree if the distance from the query point to the splitting plane is smaller than the best distance found so far — if the plane itself is farther away than the current best, nothing on the far side of it can possibly be closer, so that whole subtree is pruned.
6. Quadtree (and Octree)
Because the split point is a fixed geometric midpoint rather than a data-dependent median, a Quadtree's shape depends only on the boundary and the capacity, not on insertion order — but it can become unbalanced if points cluster tightly in one quadrant, since that quadrant keeps subdividing while the empty ones do not.
An Octree is the direct 3D generalization of a Quadtree: instead of splitting a square into four quadrants, it splits a cube into eight octants (adding a z-axis midpoint alongside x and y), recursing the same way until each octant holds few enough points. Octrees are the standard spatial index for 3D graphics, point-cloud processing, and voxel-based collision detection, for exactly the same reason Quadtrees work well in 2D: most of empty space gets pruned in one comparison per level.
7. R-Tree
Unlike the k-d Tree or Quadtree, an R-Tree indexes arbitrary shapes and rectangles — not just points — which is why it is the standard index used by GIS systems and spatial databases such as PostGIS for polygons, road segments, and other extended geometry. This lesson covers the R-Tree at the conceptual level only; a full implementation is not included here, since production R-Tree variants (R*-tree and others) depend on a node-splitting heuristic that decides how to divide an overflowing node's entries between two new bounding boxes while minimizing overlap and wasted area — a nontrivial optimization problem in its own right, well beyond what a short lesson section can responsibly cover.
The sketch below shows only the overlap-driven query idea — inserting new entries into a single, never-split node, and then walking the box hierarchy to answer an overlap query. It intentionally has no splitting logic and is not a usable R-Tree implementation on its own:
class BoundingBox:
def __init__(self, min_x, min_y, max_x, max_y):
self.min_x, self.min_y = min_x, min_y
self.max_x, self.max_y = max_x, max_y
def overlaps(self, other):
return not (other.min_x > self.max_x or other.max_x < self.min_x or
other.min_y > self.max_y or other.max_y < self.min_y)
def expand_to_include(self, other):
return BoundingBox(min(self.min_x, other.min_x), min(self.min_y, other.min_y),
max(self.max_x, other.max_x), max(self.max_y, other.max_y))
class RTreeLeaf:
def __init__(self, box, data):
self.box, self.data = box, data
class RTreeNode:
# NOTE: a real R-tree chooses the child needing the least box enlargement
# and splits a node once it overflows, using an area- or overlap-minimizing
# heuristic (quadratic split, R*-tree, ...). This sketch skips all of that
# -- it exists only to show how an overlap query descends the box tree.
def __init__(self):
self.entries = [] # list of (box, RTreeLeaf)
self.box = None
def insert(self, box, data):
self.entries.append((box, RTreeLeaf(box, data)))
self.box = box if self.box is None else self.box.expand_to_include(box)
def query(self, region):
results = []
if self.box is not None and not self.box.overlaps(region):
return results
for box, leaf in self.entries:
if box.overlaps(region):
results.append(leaf.data)
return results
8. Complexity Comparison Table
| Structure | Build Time | Query Time | Best For |
|---|---|---|---|
| Interval Tree | $O(n \log n)$ | $O(\log n)$ for one overlap; $O(\log n + k)$ for all overlaps | Overlap detection, scheduling conflicts |
| Range Tree (1D) | $O(n \log n)$ | $O(\log n + k)$ | Static sorted 1D data, many repeated range lookups |
| k-d Tree | $O(n \log n)$ | $O(\log n)$ average nearest-neighbor (worst case $O(n)$); range search roughly $O(\sqrt{n} + k)$ in 2D | Nearest-neighbor search, low-dimensional range search |
| Quadtree | $O(n \log n)$ average (depends on point distribution) | $O(\log n + k)$ average (degrades toward $O(n)$ if points cluster) | Uniform 2D partitioning, collision detection, spatial grids |
| R-Tree | $O(n \log n)$ (via repeated insert, or faster with bulk-loading) | $O(\log n + k)$ average (worst case $O(n)$ with heavy box overlap) | Indexing rectangles/polygons, GIS, spatial databases |
9. Applications & Alternatives
Where each structure shows up in practice
- Interval Tree: calendar and meeting-room conflict detection — does a proposed booking overlap any existing one?
- k-d Tree: nearest-neighbor search — "find the closest restaurant to my current location," or the nearest point in a k-nearest-neighbors classifier.
- Quadtree: game engines and collision detection — quickly narrow down which pairs of on-screen objects are even close enough to check for a collision.
- R-Tree: GIS and mapping systems — indexing roads, building footprints, and other geometry so a map view only loads what's on screen.
As with the Fenwick and Segment Trees discussed earlier in this series, the right choice here is defined by the query shape, not by implementation simplicity: a sorted array with binary search answers 1D range queries in $O(\log n + k)$ with no extra structure at all when the data is static, but it cannot handle overlapping intervals or efficient updates the way an Interval Tree or Range Tree can.
10. Exercises
Easy
- Given the intervals
(1,3),(2,6),(8,10),(15,18), build an Interval Tree and usesearch_overlapto determine whether any two of them overlap. Which pair overlaps, and which single call finds it?
Medium
- Using the k-d tree built in the hand-trace above — from
(3,6),(17,15),(13,15),(6,12),(9,1),(2,7),(10,19)— find the nearest neighbor to the query point(11, 17), either by hand (compute each distance directly, then confirm with a trace of the recursive search) or by running the code above.
Hard
- Extend the Quadtree code above to support efficient collision detection for many moving points: after each frame updates positions, query the quadtree with a small rectangle centered on each point to find its nearby neighbors cheaply, instead of comparing every pair of points. Discuss when to rebuild the quadtree from scratch each frame versus incrementally removing and reinserting only the points that moved.
Review rhythm: tomorrow, reconstruct the Interval Tree's max_end pruning rule and the k-d Tree's pruning rule without notes; next week, implement the Quadtree range query in all three languages from memory; several weeks later, identify which of these five structures fits a new spatial or interval problem you encounter, and justify it against the alternatives in the comparison table.
11. Summary Cheat Sheet
- Interval Tree: augmented BST keyed on interval start, each node stores the max endpoint in its subtree, prunes on overlap queries in $O(\log n)$.
- Range Tree (1D): balanced BST over sorted points, splits at the split node into canonical subtrees, answers range queries in $O(\log n + k)$.
- k-d Tree: alternates the splitting axis by depth, prunes nearest-neighbor search using the splitting-plane distance.
- Quadtree: splits 2D space into four fixed quadrants by spatial midpoint (not by point position); Octree is its 3D generalization with eight children.
- R-Tree: groups objects by bounding rectangle into a hierarchy of boxes, prunes any subtree whose box doesn't overlap the query region.
- Fenwick/Segment Trees solve a different problem — range aggregates over a fixed sorted array index — not overlap or multi-dimensional region queries.
- Choose by query shape: overlapping intervals, static 1D ranges, nearest neighbor, uniform spatial partitioning, or indexing extended shapes each point to a different structure above.