Building & Querying Segment Trees
In the introduction, we learned what a segment tree is and why it exists. Now we'll see exactly how it's built and how range queries work, with full code, step-by-step traces, and interactive animations.
Building the Segment Tree
The Idea
Building a segment tree is a classic divide and conquer algorithm:
- Base case: If the range has one element (
start == end), it's a leaf. Store the array value directly. - Recursive case: Split the range at
mid = (start + end) / 2. Recursively build the left child for[start, mid]and the right child for[mid+1, end]. Then set this node's value toleft + right(merge).
The recursion naturally produces a bottom-up construction: leaves are filled first, then their parents, all the way up to the root.
The Pseudocode
if start == end: // leaf: single element
tree[node] = arr[start]
return
mid = (start + end) / 2
build(arr, tree, 2*node, start, mid) // left child
build(arr, tree, 2*node+1, mid+1, end) // right child
tree[node] = tree[2*node] + tree[2*node+1] // merge
Why 2*node and 2*node+1?
This is the same indexing scheme as a binary heap. If the current node is at index node in the array:
- Left child:
2 * node - Right child:
2 * node + 1
Starting from node 1 (root), the entire tree maps cleanly into a flat array. No pointers needed.
Walkthrough: Building for [1, 3, 5, 7]
Let's trace every recursive call:
| Call | Node | Range | Action | tree[] After |
|---|---|---|---|---|
| 1 | 1 | [0-3] | Split at mid=1. Recurse left (node 2) and right (node 3). | [_, _, _, _, _, _, _, _] |
| 2 | 2 | [0-1] | Split at mid=0. Recurse left (node 4) and right (node 5). | [_, _, _, _, _, _, _, _] |
| 3 | 4 | [0-0] | Leaf! tree[4] = arr[0] = 1 | [_, _, _, _, 1, _, _, _] |
| 4 | 5 | [1-1] | Leaf! tree[5] = arr[1] = 3 | [_, _, _, _, 1, 3, _, _] |
| 5 | 2 | [0-1] | Both children done. tree[2] = tree[4] + tree[5] = 1 + 3 = 4 | [_, _, 4, _, 1, 3, _, _] |
| 6 | 3 | [2-3] | Split at mid=2. Recurse left (node 6) and right (node 7). | [_, _, 4, _, 1, 3, _, _] |
| 7 | 6 | [2-2] | Leaf! tree[6] = arr[2] = 5 | [_, _, 4, _, 1, 3, 5, _] |
| 8 | 7 | [3-3] | Leaf! tree[7] = arr[3] = 7 | [_, _, 4, _, 1, 3, 5, 7] |
| 9 | 3 | [2-3] | tree[3] = tree[6] + tree[7] = 5 + 7 = 12 | [_, _, 4, 12, 1, 3, 5, 7] |
| 10 | 1 | [0-3] | tree[1] = tree[2] + tree[3] = 4 + 12 = 16 | [_, 16, 4, 12, 1, 3, 5, 7] |
Build Complexity
Time: O(n), Each of the 2n-1 nodes is visited exactly once. The merge at each internal node is O(1). Total: O(n).
Space: O(4n) for the tree array. O(log n) for the recursion stack.
▶ Build Segment Tree Animation
Watch the tree being built bottom-up from arr = [1, 3, 5, 7]. The call stack shows the recursion depth, and the tree fills in as leaves are hit and parents merge.
C++ Implementation: Build
void build(const vector<int>& arr, vector<int>& tree,
int node, int start, int end) {
if (start == end) {
// Leaf node: store the array element
tree[node] = arr[start];
return;
}
int mid = (start + end) / 2;
// Recursively build children
build(arr, tree, 2 * node, start, mid);
build(arr, tree, 2 * node + 1, mid + 1, end);
// Internal node: merge children
tree[node] = tree[2 * node] + tree[2 * node + 1];
}
// Usage:
// vector<int> arr = {1, 3, 5, 7};
// vector<int> tree(4 * arr.size(), 0);
// build(arr, tree, 1, 0, arr.size() - 1);
Range Queries
Now the powerful part. Given the built tree, we want to compute sum(l, r) in O(log n).
The Three Cases
At each recursive call, we're at a node that covers range [start, end] and we want the sum of [l, r]. There are exactly three possibilities:
Case 1: Total Overlap (the range is fully inside the query)
If l ≤ start AND end ≤ r, then the node's entire range is inside our query range. Return the node's value directly. No need to recurse further, this is the "shortcut" that makes segment trees fast.
Case 2: No Overlap (the range is completely outside the query)
If start > r OR end < l, there's zero overlap. Return the identity element (0 for sum, ∞ for min, −∞ for max). This entire subtree is irrelevant.
Case 3: Partial Overlap
The node's range straddles the query boundary. We can't use this node's value directly because it covers elements we don't want. Recurse into both children and merge the results.
The Pseudocode
if r < start or end < l: // Case 2: no overlap
return 0
if l ≤ start and end ≤ r: // Case 1: total overlap
return tree[node]
// Case 3: partial overlap — recurse both
mid = (start + end) / 2
leftSum = query(tree, 2*node, start, mid, l, r)
rightSum = query(tree, 2*node+1, mid+1, end, l, r)
return leftSum + rightSum
Walkthrough: query(1, 3) on [1, 3, 5, 7]
We want the sum of arr[1] + arr[2] + arr[3] = 3 + 5 + 7 = 15.
| Call | Node | Range | Query [l,r] | Case | Result |
|---|---|---|---|---|---|
| 1 | 1 | [0-3] | [1-3] | Partial (0 < 1) | Recurse both |
| 2 | 2 | [0-1] | [1-3] | Partial (0 < 1) | Recurse both |
| 3 | 4 | [0-0] | [1-3] | No overlap! (0 < 1) | Return 0 |
| 4 | 5 | [1-1] | [1-3] | Total overlap! | Return 3 |
| 5 | 2 | [0-1] | 0 + 3 = 3 | ||
| 6 | 3 | [2-3] | [1-3] | Total overlap! | Return 12 |
| 7 | 1 | [0-3] | 3 + 12 = 15 ✔ |
We visited only 5 nodes (out of 7 total). Node 6 and 7 were never touched because their parent (node 3) was a total overlap. In larger trees, this pruning saves enormous amounts of work.
▶ Range Query Animation: sum(1, 3)
Green nodes = fully inside query range (return value). Red = outside (return 0). Orange = partial overlap (recurse). Watch the recursion unfold.
A Harder Example: query(2, 5) on [1, 3, 5, 7, 9, 11, 2, 4]
This example uses the larger 8-element array. The query wants sum of arr[2..5] = 5 + 7 + 9 + 11 = 32.
| Node | Range | Case | Result |
|---|---|---|---|
| 1 | [0-7] | Partial (0 < 2) | Recurse both |
| 2 | [0-3] | Partial (0 < 2) | Recurse both |
| 4 | [0-1] | No overlap (1 < 2) | Return 0 |
| 5 | [2-3] | Total overlap! | Return 12 |
| 3 | [4-7] | Partial (7 > 5) | Recurse both |
| 6 | [4-5] | Total overlap! | Return 20 |
| 7 | [6-7] | No overlap (6 > 5) | Return 0 |
| 1 | 12 + 20 = 32 ✔ |
Only 7 nodes visited. Nodes 5 and 6 provided the answer directly, their children were never touched. The no-overlap branches (4 and 7) were pruned instantly.
C++ Implementation: Query
int query(const vector<int>& tree, int node,
int start, int end, int l, int r) {
// Case 2: No overlap
if (r < start || end < l)
return 0;
// Case 1: Total overlap
if (l <= start && end <= r)
return tree[node];
// Case 3: Partial overlap
int mid = (start + end) / 2;
int leftSum = query(tree, 2 * node, start, mid, l, r);
int rightSum = query(tree, 2 * node + 1, mid + 1, end, l, r);
return leftSum + rightSum;
}
// Usage:
// int result = query(tree, 1, 0, n - 1, l, r);
Query Complexity: Why O(log n)?
Let's prove this more carefully. The claim is that a range query visits at most O(log n) nodes.
The Counting Argument
Consider any level of the tree. There are at most 2level nodes at that level. For a query [l, r]:
- At most 2 nodes at each level have partial overlap (one touching the left boundary
l, one touching the right boundaryr). These are the only nodes where we recurse into both children. - All other nodes visited at that level are total-overlap (we stop) or no-overlap (we stop).
Since the tree has ⌈log2 n⌉ + 1 levels, we recurse into both children at most O(log n) times. Each such split adds one node to the "frontier." The total nodes visited is bounded by 4 · log n (more precisely, at most 4 · ⌈log2 n⌉ - 5 for n ≥ 3).
[l, r] as a "band" sweeping across the tree from left to right. At the top levels, this band might span multiple nodes. But as we go deeper, the band gets split and eventually matches node boundaries exactly. The O(log n) bound comes from the fact that the band can "straddle" at most one boundary per level.
Complete Implementation
class SegmentTree {
int n;
vector<int> tree;
void build(const vector<int>& arr, int node, int s, int e) {
if (s == e) {
tree[node] = arr[s];
return;
}
int mid = (s + e) / 2;
build(arr, 2 * node, s, mid);
build(arr, 2 * node + 1, mid + 1, e);
tree[node] = tree[2 * node] + tree[2 * node + 1];
}
int query(int node, int s, int e, int l, int r) {
if (r < s || e < l) return 0;
if (l <= s && e <= r) return tree[node];
int mid = (s + e) / 2;
return query(2 * node, s, mid, l, r)
+ query(2 * node + 1, mid + 1, e, l, r);
}
public:
SegmentTree(const vector<int>& arr)
: n(arr.size()), tree(4 * n, 0) {
build(arr, 1, 0, n - 1);
}
int query(int l, int r) {
return query(1, 0, n - 1, l, r);
}
};
Common Mistakes
- Off-by-one in mid: The split must be
mid = (start + end) / 2, with left covering[start, mid]and right covering[mid+1, end]. Getting this wrong causes missing or double-counted elements. - Wrong identity value: For sum, return 0 on no-overlap. For min, return
INT_MAX. For max, returnINT_MIN. Using the wrong identity silently produces wrong answers. - Insufficient array size: Always allocate
4 * n. Allocating2 * ncauses out-of-bounds for non-power-of-2 sizes. - Mixing 0-indexed and 1-indexed: The tree is 1-indexed (root at 1), but the array can be 0-indexed. Be consistent.
Summary
- Build: Recursive divide-and-conquer. Leaves store array values, internals merge children. O(n) time.
- Query: Three cases at each node — total overlap (return value), no overlap (return identity), partial (recurse both). O(log n) time.
- At most 2 partial-overlap nodes per level, so the total work is bounded by the tree height.
- The tree array uses 1-based indexing with 4n space: children of node
iare2iand2i+1. - For a faster, non-recursive alternative, see the Iterative Segment Trees post.
In the next post, we'll see how point updates work and set the stage for lazy propagation.