Data Structures · Lesson 16 of 16

Segment Trees and Fenwick Trees

Answer range sum and range minimum queries with updates in O(log n) using prefix sums, Fenwick trees (binary indexed trees) and segment trees in Python.

  • Advanced
  • 18 min read
  • 4 objectives

Before this lessonLesson 15: Union-Find (Disjoint Set Union)

What you will learn

  • See why prefix sums break when data changes
  • Implement a Fenwick tree for point updates and prefix sums
  • Implement a segment tree for any associative range query
  • Choose between the two using a complexity table

Your Progress

0 of 16 lessons 0%

  • Lessons0 / 16
  • Completed0
  • Est. time left~ 4 hours

Create a free account to keep your progress on every device.

Tip: pressing Next marks this lesson complete automatically.

Imagine a dashboard showing sales per day. Users ask "total sales from day 30 to day 90" thousands of times a second, while new orders keep changing individual days. Summing the range each time is O(n) per query. Precomputing prefix sums makes queries O(1), but then a single update costs O(n). This lesson covers two structures that make both operations O(log n): the Fenwick tree and the segment tree.

Starting point: prefix sums

A prefix sum array stores prefix[i] = a[0] + ... + a[i-1]. The sum of a[l..r] is then prefix[r+1] - prefix[l], one subtraction. This is the best tool when the data never changes.

from itertools import accumulate

sales = [5, 3, 7, 2, 6, 1, 4]
prefix = [0] + list(accumulate(sales))
print(prefix)

def range_sum(l, r):          # inclusive
    return prefix[r + 1] - prefix[l]

print(range_sum(1, 3), range_sum(0, 6))
Output
[0, 5, 8, 15, 17, 23, 24, 28]
12 28

The problem: if sales[1] changes, every prefix value after it is wrong and must be rebuilt, which is O(n). With many updates and many queries, that is too slow.

Fenwick tree (binary indexed tree)

A Fenwick tree, or binary indexed tree (BIT), is an array where each slot stores the sum of a block of elements. The size of the block is decided by the lowest set bit of the index, i & -i. Using 1-based indices:

  • Slot 4 (binary 100) covers 4 elements: 1 to 4.
  • Slot 6 (binary 110) covers 2 elements: 5 to 6.
  • Slot 7 (binary 111) covers 1 element: 7.

A prefix sum up to i adds a few blocks, jumping with i -= i & -i. An update at i fixes every block that contains i, jumping with i += i & -i. Each loop runs at most log2 n times.

class Fenwick:
    def __init__(self, n):
        self.n = n
        self.tree = [0] * (n + 1)      # index 0 unused

    def add(self, i, delta):           # i is 0-based for callers
        i += 1
        while i <= self.n:
            self.tree[i] += delta
            i += i & -i

    def prefix(self, i):               # sum of a[0..i-1]
        total = 0
        while i > 0:
            total += self.tree[i]
            i -= i & -i
        return total

    def range_sum(self, l, r):         # inclusive, 0-based
        return self.prefix(r + 1) - self.prefix(l)

sales = [5, 3, 7, 2, 6, 1, 4]
fw = Fenwick(len(sales))
for i, v in enumerate(sales):
    fw.add(i, v)

print(fw.range_sum(1, 3))
fw.add(1, 10)                           # day 1 gets 10 more sales
print(fw.range_sum(1, 3), fw.range_sum(0, 6))
Output
12
22 38

The Fenwick tree is tiny (one array, about 15 lines) and fast. Its limitation is that it relies on subtraction to turn two prefix sums into a range, so it naturally handles sums and counts but not range minimum or maximum.

for i in [4, 6, 7, 12]:
    low = i & -i
    print(f"slot {i:>2} ({i:04b}) covers {low} element(s): {i - low + 1}..{i}")
Output
slot  4 (0100) covers 4 element(s): 1..4
slot  6 (0110) covers 2 element(s): 5..6
slot  7 (0111) covers 1 element(s): 7..7
slot 12 (1100) covers 4 element(s): 9..12

Segment tree

A segment tree is a binary tree where each node stores the answer for a range: the root covers the whole array, its children cover the left and right halves, and leaves cover single elements. It works for any associative operation: sum, min, max, gcd, even "count of values above a threshold". An iterative version stores it in an array of size 2n, with leaves at positions n to 2n - 1 and node i's children at 2i and 2i + 1.

class SegmentTree:
    def __init__(self, data, op, identity):
        self.n = len(data)
        self.op, self.identity = op, identity
        self.t = [identity] * self.n + list(data)
        for i in range(self.n - 1, 0, -1):
            self.t[i] = op(self.t[2 * i], self.t[2 * i + 1])

    def update(self, i, value):           # set a[i] = value
        i += self.n
        self.t[i] = value
        while i > 1:
            i //= 2
            self.t[i] = self.op(self.t[2 * i], self.t[2 * i + 1])

    def query(self, l, r):                # inclusive l..r
        res_left = res_right = self.identity
        l += self.n
        r += self.n + 1
        while l < r:
            if l & 1:
                res_left = self.op(res_left, self.t[l]); l += 1
            if r & 1:
                r -= 1; res_right = self.op(self.t[r], res_right)
            l //= 2
            r //= 2
        return self.op(res_left, res_right)

temps = [21, 18, 25, 17, 30, 22, 19]
mins = SegmentTree(temps, min, float("inf"))
sums = SegmentTree(temps, lambda a, b: a + b, 0)

print(mins.query(0, 2), mins.query(2, 5), sums.query(0, 6))
mins.update(3, 26)
print(mins.query(2, 5))
Output
18 17 152
22

The same class answered range minimum and range sum just by passing a different operation and its identity value (the value that changes nothing: 0 for sum, infinity for min). A query touches at most about 2 log2 n nodes, and an update walks from one leaf to the root.

Checking against brute force

Tricky index code deserves a test. Compare your structure with the obvious slow answer on random data; this catches off-by-one bugs quickly.

import random

class SegmentTree:
    def __init__(self, data):
        self.n = len(data)
        self.t = [0] * self.n + list(data)
        for i in range(self.n - 1, 0, -1):
            self.t[i] = max(self.t[2 * i], self.t[2 * i + 1])
    def update(self, i, v):
        i += self.n; self.t[i] = v
        while i > 1:
            i //= 2; self.t[i] = max(self.t[2 * i], self.t[2 * i + 1])
    def query(self, l, r):
        best = float("-inf"); l += self.n; r += self.n + 1
        while l < r:
            if l & 1: best = max(best, self.t[l]); l += 1
            if r & 1: r -= 1; best = max(best, self.t[r])
            l //= 2; r //= 2
        return best

random.seed(1)
data = [random.randint(0, 100) for _ in range(50)]
st = SegmentTree(data)
ok = True
for _ in range(1000):
    if random.random() < 0.3:
        i, v = random.randrange(50), random.randint(0, 100)
        data[i] = v; st.update(i, v)
    else:
        l = random.randrange(50); r = random.randrange(l, 50)
        ok &= st.query(l, r) == max(data[l:r + 1])
print("all queries match:", ok)
Output
all queries match: True

Going further

  • Range updates ("add 5 to every day from 10 to 20") use a segment tree with lazy propagation, which postpones updates to children until they are needed. It keeps both operations O(log n).
  • A Fenwick tree over a difference array supports range add with point query using the same 15 lines.
  • Real uses include leaderboards (rank = count of scores above you), time-series dashboards, competitive programming and computational geometry.

Complexity

                     Plain array   Prefix sums   Fenwick tree     Segment tree
build                O(1)          O(n)          O(n log n)*      O(n)
point update         O(1)          O(n)          O(log n)         O(log n)
range sum            O(n)          O(1)          O(log n)         O(log n)
range min/max        O(n)          not supported not supported**  O(log n)
range update         O(n)          O(n)          O(log n)***      O(log n) with lazy
memory               n             n + 1         n + 1            2n (or 4n recursive)

*   O(n) with a linear build trick
**  only for special cases such as values that only grow
*** range add with point query, via a difference array

Rule of thumb: if data never changes, use prefix sums. If you need sums or counts with updates, a Fenwick tree is smallest and simplest. If you need min, max or another operation, or range updates, reach for a segment tree.

Recap

  • Prefix sums give O(1) range sums but O(n) updates.
  • A Fenwick tree uses the lowest set bit (i & -i) to get O(log n) updates and prefix sums in one small array.
  • A segment tree stores range answers in a binary tree and supports any associative operation in O(log n).
  • Use 1-based indices for Fenwick trees and identity values for segment trees.
  • Test index-heavy structures against brute force on random data.
# Write your solution here

Finished reading? Mark this lesson complete to track your progress.

Last lessonFinish Data StructuresMark this lesson complete and pick your next course.