AlgoScope

Interval, kd and Range Trees

structureadvancedTime O(log n) interval search, O(sqrt n + k) kd range, O(log^2 n + k) range treeSpace O(n), or O(n log n) for the range tree

Each of these is a binary search tree plus one idea. An interval tree keys on left endpoints and stores at every node the largest right endpoint in its subtree, so an overlap search can drop a whole subtree by comparing one number. A kd-tree compares x at one level and y at the next, so every node is a splitting line across the plane and a range search enters only the sides the rectangle touches. A range tree is a balanced BST on x whose nodes each keep their subtree's y values sorted, so a rectangle query becomes O(log n) canonical subtrees, each answered by a binary search in its own list. Three shapes of the same move: a search tree on one coordinate, augmented with just enough about the other to prune.

(3,7) x(5,15) y(8,18) x(10,10) x(15,5) y(12,12) x(18,8) y

Which points lie in x 4-13, y 6-14? A kd-tree is a BST that compares x at even depths and y at odd depths, so each node is a splitting line, vertical or horizontal, marked after its point. A range search visits a node, reports it if it is inside, and descends only into the sides the rectangle actually reaches across the split.

Check your understanding

The player pauses before each decision in this run and asks what happens next. Here are all 6, with their answers.

  1. (10,10) splits on x = 10 and the rectangle's x range is 4-13. Which sides can hold points in the rectangle?

    • Both sides
    • Left side only
    • Right side only
    • Neither side

    Answer: Both sides. The low side is reachable when the range starts at or below the split, the high side when it ends at or above it.

  2. (5,15) splits on y = 15 and the rectangle's y range is 6-14. Which sides can hold points in the rectangle?

    • Both sides
    • Left side only
    • Right side only
    • Neither side

    Answer: Left side only. The low side is reachable when the range starts at or below the split, the high side when it ends at or above it.

  3. (3,7) splits on x = 3 and the rectangle's x range is 4-13. Which sides can hold points in the rectangle?

    • Both sides
    • Left side only
    • Right side only
    • Neither side

    Answer: Right side only. The low side is reachable when the range starts at or below the split, the high side when it ends at or above it.

  4. (15,5) splits on y = 5 and the rectangle's y range is 6-14. Which sides can hold points in the rectangle?

    • Both sides
    • Left side only
    • Right side only
    • Neither side

    Answer: Right side only. The low side is reachable when the range starts at or below the split, the high side when it ends at or above it.

  5. (12,12) splits on x = 12 and the rectangle's x range is 4-13. Which sides can hold points in the rectangle?

    • Both sides
    • Left side only
    • Right side only
    • Neither side

    Answer: Both sides. The low side is reachable when the range starts at or below the split, the high side when it ends at or above it.

  6. (18,8) splits on y = 8 and the rectangle's y range is 6-14. Which sides can hold points in the rectangle?

    • Both sides
    • Left side only
    • Right side only
    • Neither side

    Answer: Both sides. The low side is reachable when the range starts at or below the split, the high side when it ends at or above it.

How it runs, step by step

  1. Which points lie in x 4-13, y 6-14? A kd-tree is a BST that compares x at even depths and y at odd depths, so each node is a splitting line, vertical or horizontal, marked after its point. A range search visits a node, reports it if it is inside, and descends only into the sides the rectangle actually reaches across the split.

    kd-tree range search.

  2. (10,10) splits on x = 10. It is inside the rectangle: report it. The rectangle's x range is 4-13, so it crosses the split: both sides must be searched.

    (10,10): Both sides.

  3. (5,15) splits on y = 15. It is outside. The rectangle's y range is 6-14, so it lies entirely on the low side: only the left subtree.

    (5,15): Left side only.

  4. (3,7) splits on x = 3. It is outside. The rectangle's x range is 4-13, so it lies entirely on the high side: only the right subtree.

    (3,7): Right side only.

  5. (15,5) splits on y = 5. It is outside. The rectangle's y range is 6-14, so it lies entirely on the high side: only the right subtree.

    (15,5): Right side only.

  6. (12,12) splits on x = 12. It is inside the rectangle: report it. The rectangle's x range is 4-13, so it crosses the split: both sides must be searched.

    (12,12): Both sides.

  7. (18,8) splits on y = 8. It is outside. The rectangle's y range is 6-14, so it crosses the split: both sides must be searched.

    (18,8): Both sides.

  8. 2 points lie in the rectangle: (10,10), (12,12), found with 6 visits out of 7 points. A kd-tree range search costs O(sqrt n + k) in two dimensions, and the same tree answers nearest-neighbour queries by the same pruning: skip a side when the split line is further away than the best point so far.

    2 points in range.

Remember

  • Interval tree: BST on left endpoints, each node stores its subtree's max right endpoint; go left only if that max reaches the query.
  • kd-tree: alternate the comparison axis by depth; a range search enters only the sides the rectangle reaches across the split.
  • Range tree: balanced BST on x with sorted y lists per node; a query is O(log n) canonical subtrees, one binary search each.

Where this is used

Operating systemsLinux virtual memory reverse mapping

The kernel builds interval trees on top of its augmented red-black tree, storing in each node the largest end address in its subtree, which is the max field from this lesson under another name. Memory management needs the reverse question constantly: when a file is truncated or a page is reclaimed, find every mapping in every process that covers this byte range, and vma_interval_tree_foreach walks only the subtrees whose max still reaches it. A plain red-black tree keyed on start address cannot answer that, because a mapping starting far to the left can still cover the address you are asking about.

Scientific computingNearest-neighbour search in SciPy and scikit-learn

scipy.spatial.KDTree and sklearn.neighbors both descend to the leaf cell holding the query point, then backtrack into a sibling only when the distance to that sibling's splitting plane is smaller than the best distance found so far. That one test, plane distance against current best radius, is what turns a comparison with every point into a visit to a handful of cells. It is also where the structure stops working: scikit-learn's documentation puts the KD tree's efficient range at D < 20, and says the ball tree was developed to address the inefficiencies of KD trees in higher dimensions, because the planes stop separating anything.

DatabasesPostgreSQL SP-GiST point indexes

SP-GiST is PostgreSQL's framework for space-partitioning trees that are deliberately not balanced, and kd_point_ops is a kd-tree on disk for the point type, sitting alongside quad_point_ops which is a quad-tree and the default. The alternating-axis test decides which child pages a bounding-box query has to read, exactly as in the code above, and the same operator class backs the <-> ordering operator, so ORDER BY location <-> target LIMIT 10 becomes a pruned descent instead of a sort of the whole table. Nothing rebalances these trees after the fact, so all the balancing has to happen at the split: spg_kd_picksplit sorts the tuples on the page it is splitting and takes the median coordinate, and its own comment notes this keeps the tree balanced even when many points share a value.

Computational geometryCGAL's range and segment trees

CGAL, the reference computational geometry library, ships a range tree in its dD Range and Segment Trees package, templated over the number of dimensions. It is what you reach for when the point set is fixed and the same rectangle query runs millions of times, because O(log^2 n + k) holds no matter how the rectangle sits relative to the data, while a kd-tree's bound sags whenever the query boundary happens to cut across many cells. The price is the O(n log n) space and a build you cannot cheaply amend, so it belongs in offline pipelines rather than in anything taking writes.

Why it works this way

Why the overlap search can commit to one side, and what that costs

Every other search here either follows an ordered key or has to try both children, so choosing a side on one max comparison looks like a guess. It is not: if the left subtree's max right endpoint is below q.lo, nothing on the left reaches the query and the right side is the only hope, and if it is not below, take the left interval i holding that max - should i itself miss the query, then i.hi >= q.lo forces i.lo > q.hi, and every interval in the right subtree starts at or after i.lo, so the right side cannot match either. The catch is that this buys exactly one answer, because the search returns the moment a node overlaps. Reporting all k matches means descending into both children whenever their stored max still reaches q.lo, which costs O(k log n), and questions about conflicting bookings or overlapping annotations almost always want the full set.

Why the median split, and why cycling the axis is only the simple version

Splitting at the median of the chosen axis is what makes the depth log n, and every pruning argument above assumes that depth; split at the first point instead and sorted input builds a linked list. Cycling the axis by depth gives the clean depth % k rule but wastes a cut whenever the points barely vary along that axis, so a cloud that is wide in x and flat in y spends half its splits separating almost nothing, and the usual nearest-neighbour construction picks the axis of greatest spread at each node instead. scipy.spatial.KDTree exposes the same tension as a flag: balanced_tree=True splits at the median for a compact tree, and turning it off splits at the midpoint of the cell, which builds faster but can leave long thin cells that the prune test rarely rejects. Either way the tree is built once and not edited, because deleting a node needs the minimum along that node's own axis taken from the right subtree rather than the inorder successor, so a changing point set is handled by rebuilding.

Why a kd range query is O(sqrt n + k) and not O(log n + k)

The nodes you actually visit are the ones whose cell the query rectangle's boundary cuts through; a cell fully inside is reported wholesale and a cell fully outside dies on one comparison. Counting how many cells a single vertical line can cut gives Q(n) = 2 + 2Q(n/4), because the line misses two of the four grandchildren, and that recurrence solves to O(sqrt n). In d dimensions the same count is O(n^(1 - 1/d)), so at d = 20 the exponent is 0.95, the boundary crosses nearly every cell and the tree is a linear scan with pointer chasing on top. scikit-learn's own numbers line up: its docs put the KD tree's efficient range at D < 20 and say the cost climbs towards O(D*N) beyond that, and its automatic algorithm choice stops using a tree at all once D > 15 and falls back to brute force. That is the prune failing, not the implementation.

Where the range tree's extra log and extra space go

The x-tree on its own finds the x-strip in O(log n) plus the size of that strip, and the strip can hold far more points than the k inside the rectangle, so the y filter has to run inside the tree rather than over its output. Hence the sorted y list at every node. A point appears in one node per level, so those lists hold n log n entries between them: that is the entire space cost, and it is why the range tree is a build-once structure. The second log is the binary search repeated inside each of the O(log n) canonical lists, even though you are hunting the same y1 in all of them. Fractional cascading deletes that repetition by storing, next to each y in a node's list, where it falls in the two children's lists, so you binary search once at the top and follow pointers down for O(log n + k).

Read more

Next up