When an interview asks you to answer something about a sub‑array repeatedly—think "sum of elements between i and j", "minimum value in a window", or "how many numbers are greater than X in a range"—the naïve O(n) scan becomes a bottleneck. The segment tree pattern gives you O(log n) per operation after an O(n) preprocessing step, making it the go‑to tool for dynamic range queries.
Why a Segment Tree?
A segment tree is a binary tree where each node stores information about a contiguous segment of the original array. The root covers the whole array, its children cover the left and right halves, and leaves represent single elements. By storing aggregates (sum, min, max, frequency map, etc.) at each node, you can answer a query by combining at most two nodes per level—hence the logarithmic cost.
When to Reach for a Segment Tree
| Signal in the statement | Typical operation | Reason to use |
|---|---|---|
| "range sum/min/max" | Query on any interval | O(log n) vs O(n) scan |
| "update a single element" | Point update that must reflect in future queries | Tree can adjust in O(log n) |
| "multiple queries after each update" | Mix of queries and updates | Rebuilding a prefix array each time would be too slow |
| "offline queries are not allowed" | Need online answers | Segment tree works online |
| "non‑commutative combine" (e.g., concatenation) | Custom merge function | Tree can store any associative operation |
If the problem mentions static data only (no updates) and the query count is low, a prefix‑sum array might be enough. Conversely, if you need to support both range queries and point updates many times, the segment tree is usually the right choice.
A Minimal Working Example in Python
Below is a compact, readable implementation that supports range sum queries and point updates. You can replace the combine function to handle min, max, or any other associative operation.
class SegmentTree:
def __init__(self, data):
self.n = len(data)
# Next power of two for easy indexing
size = 1
while size < self.n:
size <<= 1
self.size = size
self.tree = [0] * (2 * size)
# Build leaves
for i, val in enumerate(data):
self.tree[size + i] = val
# Build internal nodes
for i in range(size - 1, 0, -1):
self.tree[i] = self.tree[2 * i] + self.tree[2 * i + 1]
def _combine(self, left, right):
return left + right # change to min/max/etc.
def update(self, idx, value):
# set data[idx] = value
pos = self.size + idx
self.tree[pos] = value
while pos > 1:
pos //= 2
self.tree[pos] = self._combine(self.tree[2 * pos], self.tree[2 * pos + 1])
def query(self, l, r):
"""Return combine over interval [l, r) (half‑open)."""
l += self.size
r += self.size
res_left, res_right = 0, 0 # identity for sum
while l < r:
if l & 1:
res_left = self._combine(res_left, self.tree[l])
l += 1
if r & 1:
r -= 1
res_right = self._combine(self.tree[r], res_right)
l //= 2
r //= 2
return self._combine(res_left, res_right)
arr = [3, 8, 7, 6, 2, 5]
st = SegmentTree(arr)
print(st.query(1, 5)) # sum of arr[1:5] => 8+7+6+2 = 23
st.update(3, 10) # change 6 -> 10
print(st.query(1, 5)) # now 8+7+10+2 = 27
Complexity
- Build: O(n) (the loop over internal nodes is linear).
- Point update: O(log n) because you walk up the tree.
- Range query: O(log n) – at most two nodes per level are visited.
Common Pitfalls to Avoid
- Off‑by‑one errors – Decide early whether your query method uses inclusive
[l, r]or half‑open[l, r). The code above uses half‑open, which aligns with Python slicing. - Identity element – For sum it is
0; for min it should be+inf; for max,-inf. Forgetting this leads to wrong results when a query touches an empty segment. - Tree size – If you allocate exactly
2*nfor a non‑power‑of‑two length, the parent‑child relationships break. Padding to the next power of two (as shown) sidesteps the issue. - Mutable combine – When the combine operation is not pure (e.g., concatenating strings), you must be careful about copying to avoid unintended sharing.
- Recursive vs iterative – Recursive implementations are easier to read but can hit recursion limits for very large inputs. The iterative version above avoids that risk.
Five Representative Practice Problems
Below are five problems you can find on typical coding platforms. They are described abstractly to avoid copying exact statements. Use the hints to shape your solution.
- Range Sum with Point Updates – Given an array of up to 10⁵ integers, support two operations:
update(i, x)(seta[i]=x) andsum(l, r)(inclusive). Hint: Classic segment tree with sum combine. - Maximum Subarray Sum in a Range – For each query
[l, r], return the maximum possible sum of a contiguous subarray inside that interval. Hint: Store four values per node – total sum, best prefix, best suffix, and best subarray. Merge using Kadane‑style logic. - Frequency of a Value in a Range – The array contains numbers from 1‑100. Queries ask for how many times
vappears betweenlandr. Hint: Keep a frequency vector of size 100 at each node; merging is element‑wise addition. - Range Minimum Query with Lazy Propagation – The array starts at zero. Two operations: add a constant
cto every element in[l, r]and query the minimum in a range. Hint: Use a lazy‑propagation segment tree; store the current minimum and a pending addition. - Longest Increasing Subsequence Length in a Range – For each query
[l, r], report the length of the longest increasing subsequence confined to that interval. Hint: This is tougher; store a small “profile” at each node (e.g., the first few elements and their LIS lengths) and combine carefully. It illustrates how segment trees can be extended with custom state.
How to Practice This
- Build the skeleton – Write a generic
SegmentTreeclass that takes acombinelambda and anidentityvalue. Run a sanity test with random updates and queries, comparing against a brute‑force loop. - Solve one problem at a time – Pick the first problem, implement the specific combine logic, and verify against the platform’s test suite. Focus on getting the edge cases (empty range, single element) right before optimizing.
- Explain your solution aloud – Use Call Assistant to rehearse a concise answer: describe the data structure, its complexity, and why it fits the problem. Speaking the explanation helps solidify the pattern for the real interview.
FAQ
- When is a Fenwick tree preferable to a segment tree? Fenwick (Binary Indexed) trees are simpler and use less memory, but they only support prefix‑type queries (e.g., sum) and point updates. If you need arbitrary range queries (like min or custom aggregates), a segment tree is more flexible.
- Do I always need lazy propagation?
Only when you have range updates (e.g., add a value to every element in
[l, r]). Point updates can be handled without laziness. - Can I store non‑numeric data in a segment tree? Yes. As long as the merge operation is associative and you have a clear identity element, you can store strings, sets, or custom objects.
- What’s the memory overhead?
An iterative tree uses roughly
2 * 2^⌈log₂ n⌉slots. For n up to 10⁵, this is under a megabyte of integers, which is negligible for interview constraints.
Frequently asked questions
When is a Fenwick tree preferable to a segment tree?
Fenwick trees are simpler and use less memory, but they only support prefix‑type queries and point updates. Use them when you only need sums (or other invertible ops) on prefixes; choose a segment tree for arbitrary range queries or custom aggregates.
Do I always need lazy propagation?
Lazy propagation is required only when the problem includes range updates (e.g., adding a constant to every element in a subarray). For pure point updates, a standard segment tree suffices.
Can I store non‑numeric data in a segment tree?
Yes. As long as the merge function is associative and you have an appropriate identity element, you can store strings, sets, or custom objects in the nodes.
What’s the memory overhead of a segment tree?
An iterative implementation uses roughly twice the next power‑of‑two size of the input array. For n ≈ 10⁵ this is under a megabyte of integers, which is negligible for interview constraints.
#coding pattern#segment trees#interview prep#data structures#python