Interval Tree
A balanced BST of intervals keyed by start, augmented with the maximum end in each subtree, to find all intervals overlapping a point or range in O(log n + k).
Definition
An interval tree stores a dynamic set of closed intervals [lo, hi] and answers overlap queries: "which stored intervals intersect [ql, qh]?" or "which contain point x?" The standard CLRS design is a balanced Binary Search Tree (Red-Black Tree or AVL Tree) keyed on lo, where each node additionally stores maxEnd, the largest hi in its subtree. The augmentation lets a search prune whole subtrees: if maxEnd of the left subtree is less than ql, nothing on the left can overlap.
Insert and delete are ordinary balanced-BST operations plus an O(1) recomputation of maxEnd along the update path, since rotations touch only a constant number of nodes. Finding one overlapping interval is O(log n); enumerating all k overlapping intervals is O(min(n, k log n)). A different, static construction (the centered interval tree) achieves O(log n + k) for reporting.
Related but different: a Segment Tree answers aggregate queries over positions in a fixed array, whereas an interval tree answers set-membership-style queries over a changing set of intervals.
Intuition
A mental model before the formal terms.
Think of meeting bookings on a calendar, sorted by start time in a BST. To find whether a new meeting [ql, qh] clashes, walk down: if the left subtree's latest end time is before ql, no meeting there can clash, so skip it entirely. That single number per node — the latest end below it — is what turns a full scan into a logarithmic search.
How it works
- Node fields:
lo,hi,maxEnd,left,right(plus height/color for balancing). - Insert: BST insert by
lo; on the way back up setmaxEnd = max(hi, maxEnd(left), maxEnd(right))and rebalance, recomputingmaxEndfor rotated nodes. - Search one overlap(ql, qh): start at the root. If the node overlaps (
lo ≤ qh and ql ≤ hi) return it. Ifleftexists andleft.maxEnd ≥ qlgo left; otherwise go right. Return null at a leaf. - Report all overlaps: recurse: skip a subtree if its
maxEnd < ql; skip the right subtree if the node'slo > qh(all starts to the right are larger); otherwise check the node and recurse into both children. - Stabbing query for point x is overlap with
[x, x].
Why it works
Going left only when left.maxEnd ≥ ql is safe: if the left subtree's largest end is before ql, no interval there can reach the query, so the answer (if any) is on the right. If we go left and find nothing, then some left interval had hi ≥ ql but all had lo > qh, and since starts on the right are even larger, no right interval overlaps either. Hence a single root-to-leaf path suffices.
The BST height is O(log n) with balancing, and maxEnd is maintainable because it depends only on a node and its two children.
Operations
| Operation | Description | Cost |
|---|---|---|
| insert(lo, hi) | Balanced BST insert by lo with maxEnd maintenance. | O(log n) |
| delete(lo, hi) | Balanced BST delete with maxEnd recomputation. | O(log n) |
| findOverlap(ql, qh) | Return one overlapping interval or null via a single descent. | O(log n) |
| findAll(ql, qh) | Report all k overlapping intervals. | O(min(n, k log n)) |
| stab(x) | All intervals containing x. | O(min(n, k log n)) |
Recognition
How to tell a problem wants this.
- A dynamic set of intervals with repeated "does anything overlap
[a, b]?" or "find everything containing pointx" queries. - Calendar booking ("My Calendar I/II/III"), collision detection, genome interval lookups.
- Sweep-line problems where the active set needs overlap queries rather than just predecessor/successor.
Interactive demo
Play, step, change the input. ← → and space work too.
Showing the closely related Segment Tree visualization.
1build(node, l, r): if l == r: tree[node] = a[l]2 else: build children over [l,mid], [mid+1,r]; tree[node] = left + right3query(node, l, r, ql, qr):4 if qr < l or r < ql: return 0 # no overlap5 if ql <= l and r <= qr: return tree[node] # total overlap6 return query(left) + query(right) # partial overlap: split7update(node, l, r, i, v): descend to leaf i, set it, recompute sums on the way upPseudocode
1findOverlap(node, ql, qh):2 while node != null:3 if node.lo <= qh and ql <= node.hi: return node4 if node.left != null and node.left.maxEnd >= ql: node = node.left5 else: node = node.right6 return nullImplementation
1import math2 3 4class IntervalTree:5 """An AVL tree of intervals keyed by START, with every node augmented by6 max_end = the largest end in its subtree. That augmentation is the whole7 idea — it lets a query prune an entire subtree in O(1). Nodes live in8 parallel lists and are addressed by index."""9 101 · The augmentation must be repaired bottom-up after every change11 def __init__(self) -> None:12 self.lo: list[int] = []13 self.hi: list[int] = []14 self.max_end: list[float] = []15 self.height: list[int] = []16 self.left: list[int] = []17 self.right: list[int] = []18 self.root = -119 20 def _h(self, i: int) -> int:21 return 0 if i == -1 else self.height[i]22 23 def _max_end_of(self, i: int) -> float:24 return -math.inf if i == -1 else self.max_end[i]25 26 def _pull(self, i: int) -> None:27 self.height[i] = 1 + max(self._h(self.left[i]), self._h(self.right[i]))28 self.max_end[i] = max(self.hi[i], self._max_end_of(self.left[i]), self._max_end_of(self.right[i]))29 302 · Rotations are the standard AVL ones plus a _pull() on each moved node31 def _rotate_right(self, y: int) -> int:32 x = self.left[y]33 self.left[y] = self.right[x]34 self.right[x] = y35 self._pull(y)36 self._pull(x)37 return x38 39 def _rotate_left(self, x: int) -> int:40 y = self.right[x]41 self.right[x] = self.left[y]42 self.left[y] = x43 self._pull(x)44 self._pull(y)45 return y46 47 def _rebalance(self, i: int) -> int:48 self._pull(i)49 bal = self._h(self.left[i]) - self._h(self.right[i])50 if bal > 1:51 if self._h(self.left[self.left[i]]) < self._h(self.right[self.left[i]]):52 self.left[i] = self._rotate_left(self.left[i])53 return self._rotate_right(i)54 if bal < -1:55 if self._h(self.right[self.right[i]]) < self._h(self.left[self.right[i]]):56 self.right[i] = self._rotate_right(self.right[i])57 return self._rotate_left(i)58 return i59 603 · Insert by start, then rebalance on the way back up61 def _insert_at(self, i: int, lo: int, hi: int) -> int:62 if i == -1:63 self.lo.append(lo)64 self.hi.append(hi)65 self.max_end.append(hi)66 self.height.append(1)67 self.left.append(-1)68 self.right.append(-1)69 return len(self.lo) - 170 if lo < self.lo[i]:71 self.left[i] = self._insert_at(self.left[i], lo, hi)72 else:73 self.right[i] = self._insert_at(self.right[i], lo, hi)74 return self._rebalance(i)75 76 def insert(self, lo: int, hi: int) -> None:77 self.root = self._insert_at(self.root, lo, hi)78 794 · The pruning rule: if the left subtree's max_end is below the query80 # start, nothing in it can overlap, so skip it entirely81 def _collect(self, i: int, lo: int, hi: int, out: list[tuple[int, int]]) -> None:82 if i == -1 or self.max_end[i] < lo:83 return # whole subtree ends too early84 self._collect(self.left[i], lo, hi, out)85 if self.lo[i] <= hi and lo <= self.hi[i]:86 out.append((self.lo[i], self.hi[i]))87 # every start in the right subtree is >= this node's start88 if self.lo[i] <= hi:89 self._collect(self.right[i], lo, hi, out)90 91 def overlapping(self, lo: int, hi: int) -> list[tuple[int, int]]:92 out: list[tuple[int, int]] = []93 self._collect(self.root, lo, hi, out)94 return out95 965 · A point query is the degenerate range query [p, p]97 def stabbing(self, p: int) -> list[tuple[int, int]]:98 return self.overlapping(p, p)- This version uses six parallel lists rather than a node class, which avoids one Python object per node — the same trade as the Aho-Corasick and suffix-tree entries.
-math.infis themax_endof an absent child, somax()needs no special case._pullrecomputes height andmax_endtogether, and every structural operation ends with one.- The nested indexing
self.left[self.left[i]]in_rebalancereads the grandchild — dense, but it is exactly the AVL double-rotation test. max_endis typedlist[float]because-math.infis a float; using a large negative integer would keep itlist[int].
Recursion depth is the tree height, about 1.44 log n, so RecursionError is unreachable for any tree that fits in memory.
max(a, b, c)takes any number of positional arguments, so the three-way maximum is one call.- Parallel lists beat a per-node class in CPython for a structure with many small nodes;
@dataclass(slots=True)is the readable middle ground. sortedcontainers.SortedListplus a bisect over starts handles many interval workloads without a custom tree.intervaltreeon PyPI is the established library and supports deletion, merging and chopping.
- Using
0as the absent-childmax_end, which breaks for negative interval ends. - Forgetting
_pullafter a rotation, leaving a stale augmentation. - Reading
self.left[i]wheniis-1, which silently reads the *last* element rather than raising — Python negative indexing makes this failure mode quieter than in C++ or JS.
- Python negative indexing makes the
-1absent-child sentinel actively dangerous:self.left[-1]reads the last element instead of failing, where C++ would be undefined behaviour and JS/TS would yieldundefined. Every access must be guarded, and the failure is quietest in Python. - Three-way maximum: C++ needs
std::max({a, b, c})with an initialiser list, while JS/TSMath.maxand Pythonmaxare variadic already. - Absent-child sentinels:
-Infinityand-math.infare natural in JS/TS and Python; C++ usesINT_MIN, which is fine only because it is never added to. - Node storage: C++ and JS/TS use records in a vector/array, Python uses parallel lists to avoid per-object overhead — three spellings of the same index-based design, all chosen to avoid pointer or reference invalidation.
Complexity
| Operation | Average | Worst | Note |
|---|---|---|---|
| Access | — | — | |
| Search | O(log n) | O(log n) | One overlapping interval. |
| Insert | O(log n) | O(log n) | |
| Delete | O(log n) | O(log n) | |
| Update | O(log n) | O(log n) | Delete + insert. |
| Overlap query (one) | O(log n) | O(log n) | |
| Overlap query (all k) | O(k log n) | O(n) | |
| Stabbing query | O(k log n) | O(n) | |
| Space | O(n) | Bounds assume a balanced underlying BST (AVL or red-black). | |
Advantages & disadvantages
- Dynamic: intervals can be added and removed between queries.
- Pruning by
maxEndgives logarithmic single-overlap search on a balanced tree. - Small augmentation over a standard balanced BST.
- Reporting all overlaps is not output-sensitive in the worst case for the augmented-BST version.
- Requires a balancing scheme underneath to guarantee bounds; an unbalanced version degrades to
O(n). - For static interval sets, sorting + Binary Search or a Segment Tree over compressed coordinates is usually simpler.
Use cases
- Calendar and resource booking with conflict detection.
- Collision detection along one axis in physics and rendering engines.
- Genomics: find all genes overlapping a region.
- Network packet classification and IP range lookups.
- Dynamic interval sets with overlap/containment queries (booking systems, collision checks).
- Sweep-line algorithms whose active set must answer "does anything overlap this?"
- Any time you would otherwise scan all intervals per query and
n·qis too large.
- Intervals are static — sort them and binary search, or build a Segment Tree over compressed endpoints.
- You only need "is the point covered?" counts — a Difference Array or sweep is simpler.
- Queries are aggregate (sum/min over a range of an array) — that is a Segment Tree problem.
Alternatives
Common mistakes
- Forgetting to recompute
maxEndafter rotations or on the way up after insert/delete. - Using strict inequalities for overlap when intervals are closed (
[1,3]and[3,5]do overlap). - Going right when
left.maxEnd >= qlfails but also not checking whether the right subtree exists. - Confusing an interval tree with a segment tree and trying to answer overlap queries with the latter.
Interview patterns
- My Calendar I: reject a booking if
findOverlapreturns non-null, then insert. - Meeting rooms II via sweep line — compare with the interval-tree approach.
- Design a range module (add/remove/query ranges) with an ordered map of disjoint intervals.
- Explain the
maxEndpruning argument.
- Recursion versus iterationIntermediate
- Greedy or dynamic programming?Advanced
- Merge IntervalsIntermediate