Hello! Welcome to your next lesson on advanced data structures.
In our last session, we mastered the Disjoint Set Union (DSU) data structure, a highly efficient tool for tracking dynamic connectivity and merging groups. Today, we shift our focus from connectivity to aggregation. We'll be implementing the Segment Tree, a powerful hierarchical data structure designed to answer questions about ranges of data, like "What is the sum, minimum, or maximum of the elements from index i to j?"
Your goal is to implement a Segment Tree from scratch, capable of performing both range queries and single-element updates in highly efficient logarithmic time. This is a common and important data structure in competitive programming and for systems that require rapid aggregation over data segments.
1. The Problem: Efficient Range Queries and Updates
Imagine you have a large array of numbers. You need to support two operations frequently:
query(i, j): Calculate an aggregate (e.g., sum, min, max) over the sub-arrayA[i...j].update(k, val): Change the value of an elementA[k]toval.
Let's consider the simple approaches:
- Naive Approach: For each query, loop from
itoj. This gives anO(n)query andO(1)update. Inefficient for many queries. - Prefix Sums: Pre-calculate an array where
prefix[i] = sum(A[0...i]). A range sumquery(i, j)becomes a fastO(1)operation (prefix[j] - prefix[i-1]). However, anupdate(k, val)forces you to recalculate all prefix sums from indexkonwards, anO(n)operation.
Neither is ideal if you have a mix of frequent queries and updates. This is precisely the problem the Segment Tree is designed to solve, offering O(log n) complexity for both operations.
Let's start by understanding why a more sophisticated approach is needed.
Segment Trees Tutorial | Range Queries | Interview Questions
This clip from Kunal Kushwaha's 'Segment Trees Tutorial' effectively sets the stage by highlighting the inefficiency of the naive O(n) approach for range queries.
Watch from 01:14 to 04:15. This section introduces the core problem and establishes the goal: achieving O(log n) performance for range queries.
2. The Structure of a Segment Tree
A Segment Tree is a binary tree that recursively partitions an array into segments.
Technical Explanation
- Leaf Nodes: Each leaf of the tree represents a single element of the input array.
- Internal Nodes: Each internal node represents the aggregation (e.g., sum, min, or max) of the elements in its corresponding range. Its range is the union of its children's ranges.
- Root Node: The root of the tree represents the entire array and holds the aggregate value for the full range
[0, n-1].
The tree is built by starting with the entire array at the root, then recursively splitting the range in half for the left and right children until each node represents a single element.

Metaphor: A Company's Financial Rollup
Think of a large company's financial reporting structure.
- Leaf Nodes: Individual sales transactions.
- Internal Nodes: A regional manager's node stores the total sales for their region, which is the sum of the sales from all the city-level managers (their children) below them.
- Root Node: The CEO's node stores the total sales for the entire company.
If you want the total sales for a specific set of cities, you don't need to add up every single transaction. You can use the pre-calculated totals from the relevant regional and city-level managers.
3. Building the Segment Tree
The Segment Tree is constructed recursively. The build process itself takes O(n) time because it visits each created node exactly once.
Technical Explanation: Recursive Build
The build function typically takes the current range [start, end] as arguments.
- Base Case: If
start == end, we are at a leaf node. We create a node and store the value of the array elementA[start]. - Recursive Step: If
start < end, we find the midpointmid = (start + end) / 2.- Recursively call
buildfor the left child with the range[start, mid]. - Recursively call
buildfor the right child with the range[mid + 1, end]. - The current node's value is the result of combining the values of its left and right children (e.g.,
left_child.value + right_child.value).
- Recursively call
Let's watch a detailed walkthrough of this recursive build process.
Segment Trees Tutorial | Range Queries | Interview Questions
Continuing with Kunal Kushwaha's tutorial, this section explains the binary tree structure and the recursive process of building it from an array.
Watch from 07:50 to 13:50. Focus on how the array is repeatedly divided by two and how the parent nodes store the sum of their children's ranges.
Under the Hood: Array-based Representation
While we conceptualize it as a tree with nodes and pointers, a Segment Tree is typically implemented using a simple array for efficiency, much like a binary heap.
- Size: The array needs to be large enough to hold the entire tree. A safe upper bound for an input array of size
nis4 * n. - Indexing:
- The root is at index
1. - For a node at index
p, its left child is at2 * pand its right child is at2 * p + 1. (Note: this is for 1-based indexing; for 0-based, it's2*p+1and2*p+2).
- The root is at index
This array representation avoids the overhead of object creation and pointer management.
4. Performing a Range Query
This is the core operation. A query(q_start, q_end) is performed recursively, starting from the root. For any given node covering a range [n_start, n_end], there are three possibilities:
-
Total Overlap: The node's range
[n_start, n_end]is completely inside the query range[q_start, q_end].- Action: Return the node's pre-computed value. There's no need to go deeper.
-
No Overlap: The node's range is completely outside the query range.
- Action: Return an identity value (0 for sum,
infinityfor min,-infinityfor max). This value won't affect the final result.
- Action: Return an identity value (0 for sum,
-
Partial Overlap: The node's range partially overlaps with the query range.
- Action: Recursively call the query on both the left and right children. Combine their results and return the combined value.
The time complexity is O(log n) because at any level of the tree, the query path will branch to at most two children, and the tree's height is O(log n).
Segment Trees Tutorial | Range Queries | Interview Questions
This segment of the video provides an excellent, detailed explanation of the three query cases and traces a full example.
Watch from 16:48 to 31:05. This is the most crucial part of the lesson. Pay close attention to how the algorithm decides whether to return a value, ignore a branch, or recurse deeper based on the three overlap conditions.
5. Performing a Point Update
An update operation is also very efficient, taking O(log n) time.
Technical Explanation
To update the value at index k in the original array:
- Recursively traverse down the tree to find the leaf node corresponding to index
k. This is like a binary search. - Once at the leaf, update its value.
- As the recursion unwinds, update the value of each parent node on the path back to the root by re-combining the values of its children.
This process only affects the nodes along the single path from the leaf to the root. Since the tree height is O(log n), the update complexity is also O(log n).
Segment Trees Tutorial | Range Queries | Interview Questions
This final clip explains how to propagate a single element update up through the Segment Tree.
Watch from 31:19 to 38:09. Observe how an update at a leaf node causes a cascading recalculation up its chain of ancestors to the root.
6. Full Python Implementation and Trade-offs
Here is a complete Python implementation for a Range Sum Query Segment Tree. Notice the use of private helper methods (_build, _query, _update) which are common in recursive data structure implementations.
import math
class SegmentTree:
"""
Segment Tree for Range Sum Queries with Point Updates.
"""
def __init__(self, nums: list[int]):
self.n = len(nums)
# The segment tree array, size 4*n is a safe upper bound
self.tree = [0] * (4 * self.n)
self.nums = nums
self._build(1, 0, self.n - 1)
def _build(self, node: int, start: int, end: int):
"""
Recursively builds the segment tree.
node: current node index in the tree array
start, end: range covered by this node
"""
if start == end:
# Leaf node: corresponds to a single element
self.tree[node] = self.nums[start]
return
mid = (start + end) // 2
left_child = 2 * node
right_child = 2 * node + 1
# Recurse on children
self._build(left_child, start, mid)
self._build(right_child, mid + 1, end)
# Internal node's value is sum of children
self.tree[node] = self.tree[left_child] + self.tree[right_child]
def _update(self, node: int, start: int, end: int, idx: int, val: int):
"""
Recursively updates the tree for a point update.
idx: index in the original array to update
val: new value
"""
if start == end:
# Leaf node found, update it
self.tree[node] = val
self.nums[idx] = val
return
mid = (start + end) // 2
left_child = 2 * node
right_child = 2 * node + 1
if start <= idx <= mid:
# idx is in the left child's range
self._update(left_child, start, mid, idx, val)
else:
# idx is in the right child's range
self._update(right_child, mid + 1, end, idx, val)
# Recalculate parent's value after child is updated
self.tree[node] = self.tree[left_child] + self.tree[right_child]
def _query(self, node: int, start: int, end: int, l: int, r: int) -> int:
"""
Recursively queries the tree for a given range [l, r].
"""
# Case 1: No Overlap
# The range [start, end] is completely outside [l, r]
if r < start or end < l:
return 0 # 0 is the identity for sum
# Case 2: Total Overlap
# The range [start, end] is completely inside [l, r]
if l <= start and end <= r:
return self.tree[node]
# Case 3: Partial Overlap
# Recurse on children and combine results
mid = (start + end) // 2
left_child = 2 * node
right_child = 2 * node + 1
p1 = self._query(left_child, start, mid, l, r)
p2 = self._query(right_child, mid + 1, end, l, r)
return p1 + p2
# Public-facing methods
def update(self, index: int, val: int):
"""Public method to update a value at a given index."""
self._update(1, 0, self.n - 1, index, val)
def query(self, left: int, right: int) -> int:
"""Public method to query the sum of a range [left, right]."""
return self._query(1, 0, self.n - 1, left, right)
# Example Usage:
nums = [1, 3, 5, 7, 9, 11]
st = SegmentTree(nums)
# Query the sum of range [1, 3] (3 + 5 + 7)
print(f"Sum of range [1, 3]: {st.query(1, 3)}") # Expected: 15
# Update the element at index 2 from 5 to 6
st.update(2, 6)
# Query the sum of range [1, 3] again (3 + 6 + 7)
print(f"Sum of range [1, 3] after update: {st.query(1, 3)}") # Expected: 16
Pros and Cons
- Pros:
O(log n)time for both range queries and point updates, which is highly efficient.- Flexible: by changing the "combine" operation in
_buildand_query, it can handle range minimum, maximum, product, GCD, etc.
- Cons:
O(n)space complexity (specifically~4n), which can be a drawback for very large arrays compared to other structures.- More complex to implement correctly compared to naive solutions.
- Standard range updates (e.g., "add 5 to all elements in
A[i...j]") require an even more complex technique called lazy propagation, which is an advanced extension of this structure.
Test your understanding!
Given the array A = [2, 5, 1, 4, 9, 3], a Segment Tree for range sum is built. To answer the query for the sum of the range [2, 4], which nodes' pre-computed values will be directly used to form the final sum? (Assume the root covers [0, 5]).
Show answer
Let's trace the query for [2, 4]:
- Root (node for
[0, 5]): Partial overlap. Recurse left and right. - Left child (node for
[0, 2]): Partial overlap with[2, 4]. Recurse. - Right child (node for
[3, 5]): Partial overlap with[2, 4]. Recurse. - From the node for
[0, 2], we go to its children:- Node for
[0, 1]: No overlap. Returns 0. - Node for
[2, 2]: Total overlap. Returns its value,A[2] = 1.
- Node for
- From the node for
[3, 5], we go to its children:- Node for
[3, 4]: Total overlap. Returns its value,A[3] + A[4] = 4 + 9 = 13. - Node for
[5, 5]: No overlap. Returns 0.
- Node for
The final result is 1 + 13 = 14. The query directly uses the pre-computed values from the nodes representing ranges [2, 2] and [3, 4].
To get a better hands-on feel for how the build, query, and update operations traverse the tree, the interactive visualization on VisuAlgo is an excellent tool.
The 'Segment Tree' visualization on VisuAlgo allows you to build, query, and update a tree step-by-step. It's a great way to solidify the recursive logic in your mind.
Go to the website and try the following: Create a Segment Tree from a random array. Perform a Range Query over a few different ranges and watch the traversal path. Perform a point Update (by setting L=R) and see which nodes get recalculated.
Conclusion
You have now learned how to implement one of the most versatile data structures for range-based problems. The Segment Tree is a staple in algorithm design and provides a powerful balance between query and update performance.
Key Takeaways:
- Core Purpose: Solves range query and point update problems in
O(log n)time. - Structure: A binary tree representing recursive partitions of an array, usually implemented with an array of size
~4n. - Query Logic: The power of the Segment Tree comes from its recursive query algorithm that efficiently combines results from nodes that totally or partially overlap with the query range.
- Update Logic: Updates are also logarithmic, as they only require traversing a single path from a leaf to the root.
Preview of the Next Lesson:
The Segment Tree is powerful but can feel a bit heavy in terms of implementation and space. For the specific case of prefix sum queries (i.e., query(0, k)), there is another clever, more space-efficient data structure called a Fenwick Tree or Binary Indexed Tree (BIT). In our next lesson, we will explore its elegant bit-manipulation-based approach and compare its trade-offs with the Segment Tree.