Data Structures & Algorithms (DSA) Module 19 – Range queries and tree queries
Fenwick trees (binary indexed trees)
How a Fenwick tree keeps the prefix sums of a changing array in one list, with updates and queries in O(log n) driven by i & -i, traced on theatre bookings.
What you will learn
- Implement prefix sums with point updates in O(log n) with the two Fenwick loops
- Apply range updates with point queries, and range sums with two trees
- Count inversions and ranks with a Fenwick tree of counts
Before you start
What you’ll learn · Before you start
What you will learn
- Implement prefix sums with point updates in O(log n) with the two Fenwick loops
- Apply range updates with point queries, and range sums with two trees
- Count inversions and ranks with a Fenwick tree of counts
Before you start
On this page
A Fenwick tree, also called a binary indexed tree, keeps the prefix sums of an array whose values keep changing. It
is a single list of n + 1 numbers: position i, counted from 1, holds the total of the last i & -i values up to i,
where i & -i is the lowest set bit of i. Adding to one value climbs through the positions whose ranges contain it,
with i += i & -i. The sum of the first i values descends through ranges that split 1 to i without overlap, with
i -= i & -i. Each loop runs at most about log₂ n times, so both operations cost O(log n), where plain prefix sums
pay O(n) for every change. A range total is the difference of two prefix sums. Peter Fenwick described the structure
in 1994 for the frequency tables of data compression, and it remains the shortest code for sums that keep changing.
Why plain prefix sums are not enough
A theatre’s booking system keeps the number of booked seats in each row and must answer questions such as “how many seats are booked in rows 4 to 12?” while bookings and cancellations keep arriving. The two obvious designs each make one operation slow:
| Design | Change one row | Total of a range of rows |
|---|---|---|
| A plain list of rows | O(1) | O(n): add up the range |
| Prefix sums | O(n): every later sum moves | O(1): one subtraction |
| Fenwick tree | O(log n) | O(log n): two prefix sums |
For 200,000 changes and 200,000 totals over 200,000 rows, either of the first two designs needs about 4 × 10¹⁰ steps in the worst case and the Fenwick tree about 10⁷: the difference between hours and well under a second. A sparse table cannot help either, because it needs data that never changes.
What each position covers
The lowest set bit of i decides how far back position i reaches. In two’s complement, -i flips every bit of i
above its lowest set bit, so i & -i keeps only that bit: 12 is 1100 in binary, so 12 & -12 is 4 and position 12
holds the total of rows 9 to 12. Odd positions hold a single row, and a power of two such as 8 holds every row up to
itself.
Each position stores the total of a range that ends at it, as long as its lowest set bit
Text description of the diagram
The numbers 1 to 16 across the top are the positions i of the tree, which are also the rows of the theatre. Each position is drawn as a bar that ends under its own number and is as long as the lowest set bit of i, the value of i & -i. The bars sit on five lines, by length, and the number at the left of each line is that length.
- Length 1: the odd positions 1, 3, 5, 7, 9, 11, 13 and 15 each cover only their own row.
- Length 2: positions 2, 6, 10 and 14 cover two rows each, for example 10 covers rows 9 and 10.
- Length 4: positions 4 and 12 cover rows 1 to 4 and rows 9 to 12.
- Length 8: position 8 covers rows 1 to 8.
- Length 16: position 16 covers all sixteen rows.
Three bars are highlighted: 11, 10 and 8. They are the positions that prefix(11) reads, and their ranges, row 11, rows 9 to 10 and rows 1 to 8, fit together without a gap or an overlap to make rows 1 to 11. An update to row 5 changes every bar that contains row 5: positions 5, 6, 8 and 16.
Two facts make the loops work. Removing the lowest set bit of i lands on the position just before the range of i, so
prefix(i) collects ranges that tile 1 to i with no gap and no overlap. Adding the lowest set bit lands on the next
position whose range contains i, so add(i, delta) reaches every total that includes row i.
Climb to update, descend to sum
The program books seats in a 16-row theatre, prints the positions each change updates and each total reads, and then checks 1,000 random changes and totals against a plain list:
Python · trace/fenwick.py
"""Booked seats per row of a 16-row theatre (synthetic), kept in a Fenwick tree, with the positions each step uses."""
import random
class Fenwick:
"""Prefix sums of values[1..n] while single values change. tree[0] is never used."""
def __init__(self, n):
self.n = n
self.tree = [0] * (n + 1)
def add(self, i, delta):
"""values[i] += delta: climb to every position whose range contains i."""
while i <= self.n:
self.tree[i] += delta
i += i & -i
def prefix(self, i):
"""values[1] + ... + values[i]: descend through ranges that split 1..i without overlap."""
total = 0
while i > 0:
total += self.tree[i]
i -= i & -i
return total
def range_sum(self, left, right):
"""values[left] + ... + values[right]."""
return self.prefix(right) - self.prefix(left - 1)
def climb(i, n):
path = []
while i <= n:
path.append(i)
i += i & -i
return path
def descend(i):
path = []
while i > 0:
path.append(i)
i -= i & -i
return path
rows = 16
seats = Fenwick(rows)
for row, change in [(5, 4), (12, 2), (3, 3), (11, 1), (5, -1)]:
seats.add(row, change)
print(f"row {row:>2} {change:+d} seats: updates positions {climb(row, rows)}")
print("tree[1..16]:", seats.tree[1:])
print(f"rows 1-11: reads positions {descend(11)}, total {seats.prefix(11)}")
print(f"rows 4-12: prefix(12) reads {descend(12)}, prefix(3) reads {descend(3)}, total {seats.range_sum(4, 12)}")
# The same bookings on a plain list, compared on 1,000 random changes and questions.
rng = random.Random(3)
plain = [0] * (rows + 1)
check = Fenwick(rows)
agree = 0
for _ in range(1000):
row = rng.randint(1, rows)
change = rng.randint(-2, 5)
plain[row] += change
check.add(row, change)
left = rng.randint(1, rows)
right = rng.randint(left, rows)
agree += check.range_sum(left, right) == sum(plain[left : right + 1])
print(f"{agree:,} of 1,000 random range totals agree with a plain list") Output
row 5 +4 seats: updates positions [5, 6, 8, 16] row 12 +2 seats: updates positions [12, 16] row 3 +3 seats: updates positions [3, 4, 8, 16] row 11 +1 seats: updates positions [11, 12, 16] row 5 -1 seats: updates positions [5, 6, 8, 16] tree[1..16]: [0, 0, 3, 3, 3, 3, 0, 6, 0, 0, 1, 3, 0, 0, 0, 9] rows 1-11: reads positions [11, 10, 8], total 7 rows 4-12: prefix(12) reads [12, 8], prefix(3) reads [3, 2], total 6 1,000 of 1,000 random range totals agree with a plain list
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 fenwick.py
JavaScript · trace/fenwick.mjs
// Booked seats per row of a 16-row theatre (synthetic), kept in a Fenwick tree, with the positions each step uses.
/** Prefix sums of values[1..n] while single values change. tree[0] is never used. */
class Fenwick {
constructor(n) {
this.n = n;
this.tree = new Array(n + 1).fill(0);
}
/** values[i] += delta: climb to every position whose range contains i. */
add(i, delta) {
for (; i <= this.n; i += i & -i) this.tree[i] += delta;
}
/** values[1] + ... + values[i]: descend through ranges that split 1..i without overlap. */
prefix(i) {
let total = 0;
for (; i > 0; i -= i & -i) total += this.tree[i];
return total;
}
/** values[left] + ... + values[right]. */
rangeSum(left, right) {
return this.prefix(right) - this.prefix(left - 1);
}
}
const climb = (i, n) => {
const path = [];
for (; i <= n; i += i & -i) path.push(i);
return path;
};
const descend = (i) => {
const path = [];
for (; i > 0; i -= i & -i) path.push(i);
return path;
};
const list = (a) => `[${a.join(', ')}]`;
/** Digits in groups of three, as Python prints them with a comma format. */
const grouped = (v) => String(v).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
const rows = 16;
const seats = new Fenwick(rows);
for (const [row, change] of [[5, 4], [12, 2], [3, 3], [11, 1], [5, -1]]) {
seats.add(row, change);
const sign = change >= 0 ? `+${change}` : `${change}`;
console.log(`row ${String(row).padStart(2)} ${sign} seats: updates positions ${list(climb(row, rows))}`);
}
console.log(`tree[1..16]: ${list(seats.tree.slice(1))}`);
console.log(`rows 1-11: reads positions ${list(descend(11))}, total ${seats.prefix(11)}`);
console.log(`rows 4-12: prefix(12) reads ${list(descend(12))}, prefix(3) reads ${list(descend(3))}, total ${seats.rangeSum(4, 12)}`);
// The same bookings on a plain array, compared on 1,000 random changes and questions (a seeded generator).
let seed = 3;
const pick = (lo, hi) => {
seed = (seed * 48271) % 2147483647;
return lo + (seed % (hi - lo + 1));
};
const plain = new Array(rows + 1).fill(0);
const check = new Fenwick(rows);
let agree = 0;
for (let t = 0; t < 1000; t++) {
const row = pick(1, rows);
const change = pick(-2, 5);
plain[row] += change;
check.add(row, change);
const left = pick(1, rows);
const right = pick(left, rows);
let total = 0;
for (let r = left; r <= right; r++) total += plain[r];
if (check.rangeSum(left, right) === total) agree++;
}
console.log(`${grouped(agree)} of 1,000 random range totals agree with a plain list`); Output
row 5 +4 seats: updates positions [5, 6, 8, 16] row 12 +2 seats: updates positions [12, 16] row 3 +3 seats: updates positions [3, 4, 8, 16] row 11 +1 seats: updates positions [11, 12, 16] row 5 -1 seats: updates positions [5, 6, 8, 16] tree[1..16]: [0, 0, 3, 3, 3, 3, 0, 6, 0, 0, 1, 3, 0, 0, 0, 9] rows 1-11: reads positions [11, 10, 8], total 7 rows 4-12: prefix(12) reads [12, 8], prefix(3) reads [3, 2], total 6 1,000 of 1,000 random range totals agree with a plain list
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node fenwick.mjs
Runs on this device, in your browser. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Booking 4 seats in row 5 updates positions 5, 6, 8 and 16, the four bars of the diagram that contain row 5. The total
of rows 1 to 11 reads positions 11, 10 and 8, three numbers that add up to 7. The total of rows 4 to 12 is
prefix(12) - prefix(3): two reads for each prefix, and the answer is 6. Position 0 is never used, because
0 & -0 is 0 and a loop from it would never move.
| Operation | Time | Extra space |
|---|---|---|
| add(i, delta): change one value | O(log n) | O(1) |
| prefix(i): the sum of the first i values | O(log n) | O(1) |
| The total of positions l to r | O(log n) | O(1) |
| Build with n separate adds | O(n log n) | O(n) |
What else the same two loops do
Several techniques reuse the two loops without changing them. Building in O(n) passes each position’s finished total to the one position above it, instead of making n separate adds. A tree over the differences of the array adds a value to a whole range of rows with two updates and reads single rows. Two trees, one over the differences and one over the differences times their positions, give range sums after range updates. Counting inversions scans the values once and asks a tree of counts how many larger values came before each one. Coordinate compression replaces large or scattered values by their ranks from 1 to n, so a tree of counts stays small. Walking down the bits finds the k-th smallest value in a tree of counts in O(log n), which gives running medians and percentiles.
Exercise and self-check
Process bookings, cancellations and range totals in a hall of n rows. The sample tests include totals above 2³¹, which a 32-bit integer cannot hold.
Exercise · Medium · Python, JavaScript
Bookings that change, and totals over ranges of rows
A hall has n rows, numbered from 1 to n, and every row starts with 0 bookings. Write range_totals(n, operations) (JavaScript: rangeTotals) that processes the operations in order:
("add", row, delta)(JavaScript:["add", row, delta]) changes the bookings of one row bydelta, which may be negative when bookings are cancelled;("sum", left, right)asks for the total bookings of rowslefttoright, both included.
Return the answers of the "sum" operations, in order. The sample tests replay the theatre of the lesson, check rows at both ends, cancellations and repeated changes, compare your answers with a plain list on random cases, and include totals larger than 2³¹.
Python · Starter code · totals.py
def range_totals(n, operations):
"""Answers of the ("sum", left, right) operations, in order; ("add", row, delta) changes one row."""
raise NotImplementedError("write range_totals") The sample tests · test_totals.py
import random
from totals import range_totals
THEATRE = [("add", 5, 4), ("add", 12, 2), ("add", 3, 3), ("add", 11, 1), ("add", 5, -1), ("sum", 1, 11), ("sum", 4, 12)]
def slow_totals(n, operations):
rows = [0] * (n + 1)
out = []
for op, a, b in operations:
if op == "add":
rows[a] += b
else:
out.append(sum(rows[a : b + 1]))
return out
def test_theatre():
"""the theatre of the lesson"""
assert range_totals(16, THEATRE) == [7, 6]
def test_both_ends_and_single_rows():
"""the first row, the last row and ranges of one row"""
ops = [("add", 1, 2), ("add", 9, 7), ("sum", 1, 1), ("sum", 9, 9), ("sum", 1, 9), ("sum", 2, 8)]
assert range_totals(9, ops) == [2, 7, 9, 0]
def test_cancellations_and_repeats():
"""negative changes and the same row changed many times"""
ops = [("add", 4, 10), ("add", 4, -3), ("add", 4, -7), ("sum", 1, 6), ("add", 4, 1), ("sum", 4, 4)]
assert range_totals(6, ops) == [0, 1]
def test_random_against_a_plain_list():
"""300 random cases, compared with a plain list"""
rng = random.Random(9)
for _ in range(300):
n = rng.randint(1, 30)
ops = []
for _ in range(rng.randint(1, 40)):
if rng.random() < 0.5:
ops.append(("add", rng.randint(1, n), rng.randint(-5, 9)))
else:
left = rng.randint(1, n)
ops.append(("sum", left, rng.randint(left, n)))
assert range_totals(n, ops) == slow_totals(n, ops), (n, ops)
def test_totals_above_two_to_the_31():
"""1,000 rows of up to 10**9 bookings: totals far above 2**31"""
rng = random.Random(4)
n = 1000
ops = [("add", row, rng.randint(10**8, 10**9)) for row in range(1, n + 1)]
ops += [("sum", 1, n), ("sum", 1, n // 2), ("sum", n // 2 + 1, n)]
answers = range_totals(n, ops)
assert answers == slow_totals(n, ops)
assert answers[0] > 2**31 JavaScript · Starter code · totals.mjs
/** Answers of the ["sum", left, right] operations, in order; ["add", row, delta] changes one row. */
export function rangeTotals(n, operations) {
throw new Error('write rangeTotals');
} The sample tests · totals.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { rangeTotals } from './totals.mjs';
const THEATRE = [['add', 5, 4], ['add', 12, 2], ['add', 3, 3], ['add', 11, 1], ['add', 5, -1], ['sum', 1, 11], ['sum', 4, 12]];
function slowTotals(n, operations) {
const rows = new Array(n + 1).fill(0);
const out = [];
for (const [op, a, b] of operations) {
if (op === 'add') rows[a] += b;
else {
let total = 0;
for (let r = a; r <= b; r++) total += rows[r];
out.push(total);
}
}
return out;
}
/** A seeded generator (Park and Miller), so every run checks the same cases. */
function generator(seed) {
let s = seed;
return (lo, hi) => {
s = (s * 48271) % 2147483647;
return lo + (s % (hi - lo + 1));
};
}
test('the theatre of the lesson', () => {
assert.deepEqual(rangeTotals(16, THEATRE), [7, 6]);
});
test('the first row, the last row and ranges of one row', () => {
const ops = [['add', 1, 2], ['add', 9, 7], ['sum', 1, 1], ['sum', 9, 9], ['sum', 1, 9], ['sum', 2, 8]];
assert.deepEqual(rangeTotals(9, ops), [2, 7, 9, 0]);
});
test('negative changes and the same row changed many times', () => {
const ops = [['add', 4, 10], ['add', 4, -3], ['add', 4, -7], ['sum', 1, 6], ['add', 4, 1], ['sum', 4, 4]];
assert.deepEqual(rangeTotals(6, ops), [0, 1]);
});
test('300 random cases, compared with a plain list', () => {
const pick = generator(9);
for (let t = 0; t < 300; t++) {
const n = pick(1, 30);
const ops = [];
for (let j = 0, m = pick(1, 40); j < m; j++) {
if (pick(0, 1) === 0) ops.push(['add', pick(1, n), pick(-5, 9)]);
else {
const left = pick(1, n);
ops.push(['sum', left, pick(left, n)]);
}
}
assert.deepEqual(rangeTotals(n, ops), slowTotals(n, ops), JSON.stringify([n, ops]));
}
});
test('1,000 rows of up to 10**9 bookings: totals far above 2**31', () => {
const pick = generator(4);
const n = 1000;
const ops = [];
for (let row = 1; row <= n; row++) ops.push(['add', row, pick(100000000, 1000000000)]);
ops.push(['sum', 1, n], ['sum', 1, n / 2], ['sum', n / 2 + 1, n]);
const answers = rangeTotals(n, ops);
assert.deepEqual(answers, slowTotals(n, ops));
assert.ok(answers[0] > 2 ** 31, 'the total of all rows is above 2**31');
}); A hint
Keep a list tree of n + 1 zeros and write the two loops of the lesson: add climbs with i += i & -i while i <= n, and prefix descends with i -= i & -i while i > 0. A range total is prefix(right) - prefix(left - 1).
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide, QuickJS): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Why the climb and the descent are right: the proofs, the steps counted and an O(n) build Pro: opens with a Pro, Premium or Ultimate pass
Both loops rest on one fact about binary numbers and one invariant each. This part proves them, counts the steps on a million positions and measures the O(n) build against n separate adds.
In this part:
- Why i & -i is the lowest set bit: two’s complement, worked
- The descent tiles 1 to i: the invariant and its proof
- The climb reaches every range that contains i, and only those
- Steps per operation, measured on a million positions
1 runnable example (Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Range updates, inversions and running percentiles, with run records Pro: opens with a Pro, Premium or Ultimate pass
The same two loops answer three more kinds of question once the right numbers are stored in the tree. This part derives each one, checks it against a plain list and runs it on up to a million values.
In this part:
- Range updates and single rows: a tree over the differences
- Range sums after range updates: two trees and the formula, worked
- Inversions of a million numbers: a tree of counts against merge sort, measured
- Running percentiles: compress the values, then walk down the bits
3 runnable examples (Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
The tree in five languages, with 64-bit totals and five bugs reproduced Pro: opens with a Pro, Premium or Ultimate pass
The tree is the same two short loops in every language, but each language has its own way to overflow or loop forever. This part runs one program in five languages, reproduces five bugs that pass a quick test, and says when a Fenwick tree is the wrong tool.
In this part:
- One Fenwick tree, five languages, every output recorded
- Five bugs that pass a quick test, reproduced
- When a Fenwick tree is the wrong tool
- Practice set
3 runnable examples (C++, Go, Java, JavaScript, Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher to mid · Warm-up
What does position i of a Fenwick tree store, and why does a prefix query read only O(log n) positions?
Position i, counted from 1, stores the total of a range that ends at i and is as long as the lowest set bit of i, the value of i & -i. Odd positions hold one value, position 12 holds four and position 16 holds sixteen. A prefix query for the first i values adds tree[i], jumps to i - (i & -i), the position just before that range, and repeats until it reaches 0. The ranges it collects tile 1 to i without a gap or an overlap, and each jump clears one set bit of i, so the query reads one position per set bit: at most about log₂ n + 1 of them. An update climbs the other way, with i + (i & -i), through the positions whose ranges contain i.
-
Mid to senior
A live quiz has 500,000 players with integer scores from 0 to 10,000,000. Scores change about 20,000 times a second, and after every change the player sees their current rank. Which structure do you use, what does each change cost, and how much memory does it need?
-
Mid to senior
A billing service gives discounts to ranges of customer IDs from 1 to 2,000,000, thousands of times a day, and finance asks for the total discount given to any range of IDs at any moment. How do you make both operations O(log n), and how do you convince a reviewer that your formula is right?
-
Fresher to mid
Your service keeps the prices of 1 million products in a Fenwick tree to answer range sums. Product now also wants the lowest price in any range of product IDs, and prices go both up and down. Can the Fenwick tree answer it? What would you use instead, and what does it cost?
Key takeaways
- A Fenwick tree is one list in which position i holds the total of the last
i & -ivalues up to i. addclimbs withi += i & -i;prefixdescends withi -= i & -i; both touch at most ⌊log₂ n⌋ + 1 positions.- A range total is
prefix(r) - prefix(l - 1), so the operation must be one that can be undone, such as addition. - Positions start at 1, because
0 & -0is 0.
References
- A new data structure for cumulative frequency tables (P. M. Fenwick, Software: Practice and Experience 24(3), 1994) (John Wiley & Sons)
- ECMAScript language specification: the binary bitwise operators (Ecma International (TC39))
- The Java Language Specification, Java SE 25: integral types and values (Oracle)
- The Go Programming Language Specification: integer overflow (The Go Authors)
- Built-in Types: bitwise operations on integer types (Python Software Foundation)
- bisect: array bisection algorithm (The Python Standard Library) (Python Software Foundation)
- Dictionary of Algorithms and Data Structures: inversion (National Institute of Standards and Technology (NIST))
- ACM/IEEE-CS/AAAI Computer Science Curricula 2023: Algorithmic Foundations (AL) (ACM, IEEE Computer Society and AAAI)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress