C Programming Data Structures Segment Trees And Binary Indexed Trees Fenwick Tree Complete Guide
Understanding the Core Concepts of C Programming data structures Segment Trees and Binary Indexed Trees Fenwick Tree
Segment Trees
Segment Trees are a versatile data structure that allows efficient querying and updating of array elements. They are particularly useful for problems involving range queries and point updates. A segment tree can be used to solve problems like finding the sum, minimum, maximum, or greatest common divisor over a range in O(log n) time, while updating an element also takes O(log n) time.
Structure and Construction
A segment tree is a binary tree where each node represents a range of the array. The root node represents the entire range (from 0 to n-1), and each leaf node represents an individual element of the array. The internal nodes represent the union of the ranges of their children.
For an array of size n
, the segment tree can be represented as an array segTree[]
where:
segTree[0]
is not used (some implementations start from index 0).segTree[1]
represents the root node.segTree[i]
has its left child atsegTree[2*i]
and right child atsegTree[2*i + 1]
.
Building the Segment Tree
The segment tree is built in a bottom-up manner. Each leaf node is initialized with an element from the array, and each internal node is computed based on its children. For a sum-based segment tree, each internal node stores the sum of its children's values.
C Code Example:
#include <stdio.h>
// Function to build the segment tree
void buildSegTree(int arr[], int segTree[], int node, int start, int end) {
if (start == end) {
// Leaf node will have a single element
segTree[node] = arr[start];
} else {
int mid = (start + end) / 2;
// Recurse on the left and right children
buildSegTree(arr, segTree, 2 * node, start, mid);
buildSegTree(arr, segTree, 2 * node + 1, mid + 1, end);
// Internal node will have the sum of both children
segTree[node] = segTree[2 * node] + segTree[2 * node + 1];
}
}
int main() {
int arr[] = {1, 3, 5, 7, 9, 11};
int n = sizeof(arr) / sizeof(arr[0]);
int segTree[4 * n]; // Maximum size of segment tree
buildSegTree(arr, segTree, 1, 0, n - 1);
// Example: Print the segment tree
for (int i = 1; i < 4 * n; i++) {
printf("%d ", segTree[i]);
}
printf("\n");
return 0;
}
Range Query
To efficiently answer range queries, we can use a recursive function that traverses the segment tree and combines results of relevant nodes.
C Code Example:
// Function to return the sum of elements in range [l, r]
int rangeQuery(int segTree[], int node, int start, int end, int l, int r) {
if (r < start || end < l) {
// Range represented by a node is completely outside the given range
return 0;
}
if (l <= start && end <= r) {
// Range represented by a node is completely inside the given range
return segTree[node];
}
// Range represented by a node is partially inside and partially outside the given range
int mid = (start + end) / 2;
int leftSum = rangeQuery(segTree, 2 * node, start, mid, l, r);
int rightSum = rangeQuery(segTree, 2 * node + 1, mid + 1, end, l, r);
return leftSum + rightSum;
}
Point Update
Updating a single element in the segment tree involves traversing from the leaf to the root and updating all relevant nodes.
C Code Example:
// Function to update a single element
void updateSegTree(int arr[], int segTree[], int node, int start, int end, int idx, int value) {
if (start == end) {
// Leaf node
arr[idx] = value;
segTree[node] = value;
} else {
int mid = (start + end) / 2;
if (start <= idx && idx <= mid) {
// If idx is in the left child, recurse on the left child
updateSegTree(arr, segTree, 2 * node, start, mid, idx, value);
} else {
// If idx is in the right child, recurse on the right child
updateSegTree(arr, segTree, 2 * node + 1, mid + 1, end, idx, value);
}
// Internal node will have the sum of both children
segTree[node] = segTree[2 * node] + segTree[2 * node + 1];
}
}
Binary Indexed Trees (Fenwick Trees)
Binary Indexed Trees are another data structure that supports efficient range queries and point updates. They are simpler to implement than segment trees and use less memory (O(n) vs. O(4n) for segment trees). BITs are particularly useful for scenarios where the range queries are cumulative (e.g., prefix sums, cumulative frequency tables).
Structure and Construction
A BIT is an array that allows efficient computation of prefix sums. Each element in the BIT array corresponds to a range of elements in the input array. The key property of BITs is that the i-th element in the BIT array represents the sum of a subset of elements in the input array, specifically the elements whose indices have binary representations ending in the lowest bit set in i
.
Updating a BIT
To update an element in a BIT, we increment the element and propagate the changes to the parent nodes.
C Code Example:
// Function to update an element at idx in the BIT
void updateBIT(int BIT[], int n, int idx, int val) {
// Index in BIT is 1 more than the input array
idx = idx + 1;
// Traverse all ancestors of BIT[idx]
while (idx <= n) {
// Add val to current node of BIT
BIT[idx] += val;
// Update index to that of parent in BIT
idx += idx & (-idx);
}
}
Prefix Sum Query
To get the prefix sum up to a certain index, we sum the relevant elements in the BIT.
C Code Example:
// Function to get the prefix sum up to a certain index
int getPrefixSum(int BIT[], int idx) {
int sum = 0;
// Index in BIT is 1 more than the input array
idx = idx + 1;
// Traverse ancestors of BIT[idx]
while (idx > 0) {
// Add current element of BIT to sum
sum += BIT[idx];
// Move index to parent node in BIT
idx -= idx & (-idx);
}
return sum;
}
Range Query
Range queries can be efficiently answered using two prefix sum queries.
C Code Example:
// Function to get the sum of elements in range [l, r]
int rangeQuery(int BIT[], int l, int r) {
// Remove prefix sum up to element before l
return getPrefixSum(BIT, r) - getPrefixSum(BIT, l - 1);
}
Comparison
- Memory Usage: Segment Trees typically use more memory (4n) than BITs (n).
- Implementation Complexity: BITs are generally easier to code and maintain.
- Range Queries: Segment Trees support more complex range queries (e.g., minimum, maximum, gcd).
- Point Updates: Both structures support efficient point updates (O(log n)).
Conclusion
Both Segment Trees and Binary Indexed Trees are powerful data structures for handling range queries and point updates efficiently. The choice between them depends on the specific requirements of the problem. For simple cumulative queries, BITs are preferable due to their simplicity and lower memory usage. However, for more complex range queries, segment trees offer greater flexibility.
Online Code run
Step-by-Step Guide: How to Implement C Programming data structures Segment Trees and Binary Indexed Trees Fenwick Tree
Segment Trees
A segment tree is a tree data structure for storing intervals or segments. It allows for Fast Query and Update operations, typically in (O(\log n)) time.
Example: Range Sum Query
Problem Statement:
Given an array arr[]
of size n
, support the following operations:
- Update
arr[i]
tonew_val
. - Get the sum of elements from index
l
tor
(both inclusive).
Solution: Let's build a segment tree for a given array and perform range sum queries:
#include <stdio.h>
#include <math.h>
// Function to build segment tree from given array
void buildSegmentTree(int arr[], int segmentTree[], int n) {
// Fill the leaves of the segment tree
for (int i = 0; i < n; i++)
segmentTree[n + i] = arr[i];
// Build the tree by calculating values of parent nodes
for (int i = n - 1; i > 0; --i)
segmentTree[i] = segmentTree[i << 1] + segmentTree[i << 1 | 1];
}
// Function to update a value in the input array and segment tree
void updateValue(int arr[], int segmentTree[], int n, int index, int newValue) {
// Update the value in array
arr[index] = newValue;
// Update the value in segment tree
for (segmentTree[index += n] = newValue; index > 1; index >>= 1)
segmentTree[index >> 1] = segmentTree[index] + segmentTree[index ^ 1];
}
// Function to get sum of elements from index `l` to `r` in the segment tree
int getSum(int segmentTree[], int n, int l, int r) {
int sum = 0;
// Loop to find the sum in the range `[l, r]`
for (l += n, r += n; l < r; l >>= 1, r >>= 1) {
if (l & 1) {
sum += segmentTree[l++];
}
if (r & 1) {
sum += segmentTree[--r];
}
}
return sum;
}
int main() {
int arr[] = {1, 3, 5, 7, 9, 11};
int n = sizeof(arr) / sizeof(arr[0]);
// Total size of segment tree
int treeSize = 2 * pow(2, int(ceil(log2(n))));
// Allocate memory for segment tree
int segmentTree[treeSize];
// Build segment tree from given array
buildSegmentTree(arr, segmentTree, n);
// Query range sums
printf("Sum of values in range [1, 3] = %d\n", getSum(segmentTree, n, 1, 4));
// Update value at index 1 to 10
updateValue(arr, segmentTree, n, 1, 10);
// Query range sums after update
printf("Sum of values in range [1, 3] after update = %d\n", getSum(segmentTree, n, 1, 4));
return 0;
}
Binary Indexed Trees (Fenwick Trees)
A Binary Indexed Tree (Fenwick Tree) is another data structure that supports fast prefix sums and updates on an array.
Example: Range Sum Query
Problem Statement:
Given an array arr[]
of size n
, support the following operations:
- Update
arr[i]
tonew_val
. - Get the sum of elements from index
0
toi
.
Solution: Let's build a Binary Indexed Tree for a given array and perform prefix sum queries:
#include <stdio.h>
// Helper function to update the Binary Indexed Tree (Fenwick Tree)
void updateBIT(int BIT[], int n, int index, int value) {
// Traverse all ancestors and add 'value' to all ancestors
for (; index <= n; index += index & (-index)) {
BIT[index] += value;
}
}
// Helper function to get the sum of elements from index 0 to `index` in the BIT
int getSumBIT(int BIT[], int index) {
int sum = 0;
// Traverse ancestors of `index` in Binary Indexed Tree to get the sum
for (; index > 0; index -= index & (-index)) {
sum += BIT[index];
}
return sum;
}
// Constructs and returns a Binary Indexed Tree for the given array of size `n`
void constructBIT(int arr[], int BIT[], int n) {
// Initialize BIT with 0
for (int i = 1; i <= n; i++) {
BIT[i] = 0;
}
// Store the actual values in BIT
for (int i = 0; i < n; i++) {
updateBIT(BIT, n, i + 1, arr[i]);
}
}
int main() {
int arr[] = {1, 3, 5, 7, 9, 11};
int n = sizeof(arr) / sizeof(arr[0]);
// Create and initialize Binary Indexed Tree
int BIT[n + 1];
constructBIT(arr, BIT, n);
// Get the sum of elements in the array from index 0 to 3 (inclusive)
printf("Sum of values in array from index 0 to 3 = %d\n", getSumBIT(BIT, 4));
// Update value at index 1 to 10
updateBIT(BIT, n, 2, 7); // Update index 2 (1-based) by 7 (10 - 3)
// Get the new sum after update
printf("Sum of values in array from index 0 to 3 after update = %d\n", getSumBIT(BIT, 4));
return 0;
}
Explanation
Segment Tree:
- The
buildSegmentTree
function initializes the segment tree from the given array. - The
updateValue
function updates an element in the array and the corresponding node in the segment tree. - The
getSum
function calculates the sum of elements in a specific range.
- The
Binary Indexed Tree (Fenwick Tree):
- The
updateBIT
function updates an element in the BIT. - The
getSumBIT
function calculates the prefix sum up to a specific index. - The
constructBIT
function initializes the BIT from the given array.
- The
Top 10 Interview Questions & Answers on C Programming data structures Segment Trees and Binary Indexed Trees Fenwick Tree
1. What is a Segment Tree and what are its primary uses?
Answer:
A Segment Tree is a data structure that allows answering range queries over an array efficiently while also supporting efficient updates to the array. It is a full binary tree where each node represents a contiguous segment of the array. The primary uses include finding the sum of elements in a range, finding the minimum or maximum in a range, and performing range updates.
2. How do you construct a Segment Tree in C?
Answer:
To construct a segment tree in C, you typically initialize a tree array (often 4 times the size of the input array). Here's a simple example for a sum query:
#include <stdio.h>
#include <stdlib.h>
void buildTree(int arr[], int tree[], int node, int start, int end) {
if (start == end) {
tree[node] = arr[start];
} else {
int mid = (start + end) / 2;
buildTree(arr, tree, 2 * node, start, mid);
buildTree(arr, tree, 2 * node + 1, mid + 1, end);
tree[node] = tree[2 * node] + tree[2 * node + 1];
}
}
int main() {
int arr[] = {1, 3, 5, 7, 9, 11};
int n = sizeof(arr) / sizeof(arr[0]);
int *tree = (int*)malloc(4 * n * sizeof(int));
buildTree(arr, tree, 1, 0, n - 1);
// tree is now constructed
free(tree);
return 0;
}
3. What is a Binary Indexed Tree (Fenwick Tree) and when is it used?
Answer:
A Binary Indexed Tree, or Fenwick Tree, is another data structure that can be used to manipulate cumulative frequency tables. It's one of the most efficient ways to maintain prefix sums of dynamic arrays. Its operations: querying the prefix sum, updating an element, and building the tree all run in O(log n) time.
4. How do you build a Binary Indexed Tree in C?
Answer:
Here's a basic example of initializing and updating a BIT:
#include <stdio.h>
void updateTree(int tree[], int n, int index, int delta){
while(index <= n){
tree[index] += delta;
index += index & (-index);
}
}
int queryTree(int tree[], int index){
int sum = 0;
while(index > 0){
sum += tree[index];
index -= index & (-index);
}
return sum;
}
int main(){
int arr[] = {1, 2, 3, 4, 5, 6};
int n = sizeof(arr) / sizeof(arr[0]);
int *tree = (int*)calloc(n + 1, sizeof(int));
for(int i = 0; i < n; i++)
updateTree(tree, n, i + 1, arr[i]);
printf("Sum of first 3 elements: %d\n", queryTree(tree, 3));
free(tree);
return 0;
}
5. Which is more suitable between a Segment Tree and a Binary Indexed Tree for a problem involving prefix sums?
Answer:
For problems that involve only prefix sums (and point updates), a Binary Indexed Tree is usually more efficient because it has a simpler implementation and lower constant factors. However, Segment Trees are more suitable when you need to perform range queries and updates.
6. How would you handle a point update in a Segment Tree?
Answer:
ToUpdate a value in a segment tree, you need to propagate the change up the tree. Here's a sample update function:
void update(int tree[], int node, int start, int end, int idx, int val) {
if (start == end) {
// Leaf node
arr[idx] += val;
tree[node] += val;
} else {
int mid = (start + end) / 2;
if(start <= idx && idx <= mid){
// If idx is in the left child
update(tree, 2 * node, start, mid, idx, val);
} else {
// If idx is in the right child
update(tree, 2 * node + 1, mid + 1, end, idx, val);
}
// Internal node will have the sum of both of its children
tree[node] = tree[2 * node] + tree[2 * node + 1];
}
}
7. How do you perform a range query in a Segment Tree?
Answer:
To query a range, you need to consider several cases to make sure you cover the range effectively:
int query(int tree[], int node, int start, int end, int l, int r) {
if(r < start || end < l){
// Range represented by a node is completely outside the given range
return 0;
}
if(l <= start && end <= r){
// Range represented by a node is completely inside the given range
return tree[node];
}
// Segment tree is partly inside and partly outside the given range
int mid = (start + end) / 2;
int p1 = query(tree, 2 * node, start, mid, l, r);
int p2 = query(tree, 2 * node + 1, mid + 1, end, l, r);
return (p1 + p2);
}
8. How can you implement lazy propagation in a Segment Tree?
Answer:
Lazy propagation is used to defer updates to the underlying array, thus reducing the number of times a node is recursively updated:
void updateRange(int tree[], int lazy[], int node, int start, int end, int l, int r, int val) {
if(lazy[node] != 0){
tree[node] += (end - start + 1) * lazy[node];
if(start != end){
lazy[node * 2] += lazy[node];
lazy[node * 2 + 1] += lazy[node];
}
lazy[node] = 0;
}
if(start > end || start > r || end < l){
// Current segment is not within range [l, r]
return;
}
if(start >= l && end <= r){
// Segment is fully within range
tree[node] += (end - start + 1) * val;
if(start != end){
// Not leaf node
lazy[node * 2] += val;
lazy[node * 2 + 1] += val;
}
return;
}
// Segment is partially within range
int mid = (start + end) / 2;
updateRange(tree, lazy, node * 2, start, mid, l, r, val);
updateRange(tree, lazy, node * 2 + 1, mid + 1, end, l, r, val);
tree[node] = tree[node * 2] + tree[node * 2 + 1];
}
int queryRange(int tree[], int lazy[], int node, int start, int end, int l, int r) {
if(start > end || start > r || end < l){
return 0;
}
if(lazy[node] != 0) {
// There are pending updates
tree[node] += (end - start + 1) * lazy[node];
if(start != end) {
lazy[node*2] += lazy[node];
lazy[node*2+1] += lazy[node];
}
lazy[node] = 0;
}
if(start >= l && end <= r){
return tree[node];
}
int mid = (start + end) / 2;
int p1 = queryRange(tree, lazy, node*2, start, mid, l, r);
int p2 = queryRange(tree, lazy, node*2+1, mid+1, end, l, r);
return (p1 + p2);
}
9. Can Segment Trees and Binary Indexed Trees be used for problems beyond sums and minimums?
Answer:
Yes, both Segment Trees and Binary Indexed Trees can be adapted to handle a variety of other queries and updates, such as finding the maximum, the greatest common divisor (GCD), the least common multiple (LCM), and even custom operations. However, more complex operations might require additional logic for handling updates and queries.
10. What are the time complexities associated with Segment Trees and Binary Indexed Trees?
Answer:
Segment Trees:
- Construction: O(n)
- Range Query: O(log n)
- Point Update: O(log n)
- Range Update with Lazy Propagation: O(log n)
Binary Indexed Trees (Fenwick Trees):
- Construction: O(n log n) via repeated point updates
- Prefix Sum Query: O(log n)
- Point Update: O(log n)
Login to post a comment.