← All Posts
DSA Series · Trees · Segment Trees · Part 2

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:

  1. Base case: If the range has one element (start == end), it's a leaf. Store the array value directly.
  2. 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 to left + 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

function build(arr, tree, node, start, end):
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:

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:

CallNodeRangeActiontree[] After
11[0-3]Split at mid=1. Recurse left (node 2) and right (node 3).[_, _, _, _, _, _, _, _]
22[0-1]Split at mid=0. Recurse left (node 4) and right (node 5).[_, _, _, _, _, _, _, _]
34[0-0]Leaf! tree[4] = arr[0] = 1[_, _, _, _, 1, _, _, _]
45[1-1]Leaf! tree[5] = arr[1] = 3[_, _, _, _, 1, 3, _, _]
52[0-1]Both children done. tree[2] = tree[4] + tree[5] = 1 + 3 = 4[_, _, 4, _, 1, 3, _, _]
63[2-3]Split at mid=2. Recurse left (node 6) and right (node 7).[_, _, 4, _, 1, 3, _, _]
76[2-2]Leaf! tree[6] = arr[2] = 5[_, _, 4, _, 1, 3, 5, _]
87[3-3]Leaf! tree[7] = arr[3] = 7[_, _, 4, _, 1, 3, 5, 7]
93[2-3]tree[3] = tree[6] + tree[7] = 5 + 7 = 12[_, _, 4, 12, 1, 3, 5, 7]
101[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.

Input Array
Tree Array [1..7]
Call Stack

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.

Why only O(log n) nodes? At each level of the tree, at most two nodes can have partial overlap (one touching the left boundary of [l,r], one touching the right boundary). All other nodes at that level are either fully inside (Case 1, we take them) or fully outside (Case 2, we skip). Since the tree has O(log n) levels, we visit at most O(2 · log n) = O(log n) partial-overlap nodes + some fully-inside nodes.

The Pseudocode

function query(tree, node, start, end, l, r):
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.

CallNodeRangeQuery [l,r]CaseResult
11[0-3][1-3]Partial (0 < 1)Recurse both
22[0-1][1-3]Partial (0 < 1)Recurse both
34[0-0][1-3]No overlap! (0 < 1)Return 0
45[1-1][1-3]Total overlap!Return 3
52[0-1]0 + 3 = 3
63[2-3][1-3]Total overlap!Return 12
71[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.

Query: sum(1, 3)
-
Trace

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.

NodeRangeCaseResult
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
112 + 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]:

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).

Visual intuition: Think of the query range [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);
    }
};
Looking for the iterative version? The iterative segment tree uses a completely different bottom-up layout and is covered in its own dedicated post.

Common Mistakes

Summary

In the next post, we'll see how point updates work and set the stage for lazy propagation.