Quant Memo
Core

Segment Trees

A segment tree answers "what is the sum, min or max over positions i to j?" in O(log n) while still letting you change individual values in O(log n). It is the standard answer when prefix sums break because the data keeps updating.

Prerequisites: Big-O Complexity, Prefix Sums, Recursion and the Call Stack

You have a series of 100,000 values — one-minute returns, say — and you keep asking for the sum over arbitrary windows. If the data never changes, Prefix Sums solve this outright: precompute running totals once and every window sum becomes one subtraction, O(1)O(1).

Now let one value change. A late correction arrives and minute 40,312 is revised. Every prefix sum after that point is now wrong, and rebuilding them costs O(n)O(n). Do that a few thousand times and the "fast" solution is the slowest thing in your pipeline. The opposite extreme — no precomputation, just loop over the window — makes updates free but each query O(n)O(n).

A segment tree refuses the trade. It makes both operations O(logn)O(\log n).

The idea: a pyramid of partial answers

Think of a knockout tournament drawn upside down. The bottom row is your raw data. Each node one level up stores the combined result for the two children below it — their sum, or their minimum, whichever aggregate you care about. The root, at the top, holds the aggregate of everything.

Two properties make this work. First, any range you ask about can be cut into a handful of nodes that already exist: a query over positions 1 to 6 in an eight-element array is not seven separate lookups, it is a couple of pre-aggregated blocks plus a couple of leaves. There are at most two such blocks per level, so a query touches O(logn)O(\log n) nodes. Second, when one leaf changes, only the nodes on the path from that leaf to the root contain it, so an update fixes exactly log2n\log_2 n values.

build O(n),query O(logn),update O(logn),space O(n)\text{build } O(n), \quad \text{query } O(\log n), \quad \text{update } O(\log n), \quad \text{space } O(n)

In words: building the pyramid once costs a single linear pass, and after that both reading a range and changing a point cost about log2n\log_2 n steps — 17 steps for 100,000 elements.

[0,7] = 31 [0,3] = 11 [4,7] = 20 [0,1]=3 [2,3]=8 [4,5]=9 [6,7]=11 1 2 5 3 4 5 7 4 sum over [2,7] = 8 + 20 = 28, two nodes instead of six additions
Each node stores the aggregate of the range beneath it. A query is answered by the smallest set of nodes that exactly tiles the requested range — at most two per level, so O(log n).

Worked example: query and update

Take [1, 2, 5, 3, 4, 5, 7, 4] and build the sum tree above. Ask for the sum over positions 2 to 7. Start at the root [0,7]: it is not fully inside the query, so split. [0,3] overlaps partially, split again; [0,1] is entirely outside, discard it; [2,3] = 8 is entirely inside, take it whole. Back on the right, [4,7] = 20 is entirely inside, take it whole. Total 8 + 20 = 28 from two node reads.

Now update position 5 from 5 to 9, a change of +4. Only the nodes containing position 5 are affected: leaf 5 → 9, then [4,5]: 9 → 13, then [4,7]: 20 → 24, then the root 31 → 35. Three internal nodes touched, and every future query is instantly correct.

class SegTree:                      # build O(n), query/update O(log n)
    def __init__(self, a):
        self.n = len(a)
        self.t = [0] * (2 * self.n)
        self.t[self.n:] = a
        for i in range(self.n - 1, 0, -1):
            self.t[i] = self.t[2*i] + self.t[2*i + 1]

    def update(self, i, val):       # walk leaf -> root, log n parents
        i += self.n
        self.t[i] = val
        while i > 1:
            i //= 2
            self.t[i] = self.t[2*i] + self.t[2*i + 1]

    def query(self, lo, hi):        # sum over [lo, hi)
        res, lo, hi = 0, lo + self.n, hi + self.n
        while lo < hi:
            if lo & 1: res += self.t[lo]; lo += 1
            if hi & 1: hi -= 1; res += self.t[hi]
            lo //= 2; hi //= 2
        return res

Worked example: the same tree, minimum instead of sum

Change the combine step from + to min and rebuild on the same array. The leaves are unchanged; [0,1] now stores min(1,2) = 1, [2,3] stores 3, [4,5] stores 4, [6,7] stores 4, and the root stores 1. Ask for the minimum over positions 2 to 7 and you take the same two nodes as before: min([2,3], [4,7]) = min(3, 4) = 3. Nothing about the traversal changed — only the operator did.

Use a segment tree when you need range queries and point updates together. Static data → prefix sums, O(1)O(1) queries. Updates in the mix → segment tree, O(logn)O(\log n) for both. Sums only, and you want less code and memory → a Fenwick Trees and Binary Indexed Trees does the same job in half the space.

The tree does not care that the operation is addition. Swap + for min, max, gcd, "count of zeros", or even a small matrix product, and the same code answers range-minimum or range-max queries. The only requirement is associativity: combining must not depend on how you bracket it, because the tree brackets it for you.

Two traps. Subtraction and division are not associative, so a "range difference" tree returns nonsense — Fenwick trees get away with subtraction only because sums have inverses. And if you need to update a whole range at once, the naive tree degrades to O(nlogn)O(n \log n); you need lazy propagation, which parks a pending update at a node and pushes it down only when someone looks. Reaching for lazy propagation before you need it is the usual over-engineering mistake.

Where it shows up

Interviewers use segment trees for "range sum with updates", "range minimum query", counting inversions during a merge, and the harder skyline and rectangle-area problems. Saying "prefix sums, unless there are updates, in which case a segment tree" is often the whole answer they want.

On a desk the pattern is everywhere once you see it. Rolling statistics over a tick stream where late corrections rewrite past bars; aggregate depth in an order book across a band of price levels while individual levels change every millisecond; risk aggregation over a book bucketed by sector or maturity, where one position moves and you need the desk total immediately; and VWAP over an arbitrary intraday window rather than a fixed one. In each case the alternative is recomputing a linear pass per update, and at market data rates that is exactly the budget you do not have.

Related concepts

Practice in interviews

Further reading

  • Cormen, Leiserson, Rivest & Stein, Introduction to Algorithms (augmented data structures)
  • Halim & Halim, Competitive Programming (data structures)
ShareTwitterLinkedIn