Skip to content

Commit cd3c465

Browse files
authored
Merge pull request #79 from 4ndrelim/branch-segmentTree
feat: Implement segment tree
2 parents 67bd654 + c5264c8 commit cd3c465

File tree

8 files changed

+406
-0
lines changed

8 files changed

+406
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Gradle is used for development.
3838
- [Monotonic Queue](src/main/java/dataStructures/queue/monotonicQueue)
3939
- Segment Tree
4040
- [Stack](src/main/java/dataStructures/stack)
41+
- [Segment Tree](src/main/java/dataStructures/segmentTree)
4142
- [Trie](src/main/java/dataStructures/trie)
4243

4344
## Algorithms
@@ -86,6 +87,7 @@ Gradle is used for development.
8687
* [AVL-tree](src/main/java/dataStructures/avlTree)
8788
* [Trie](src/main/java/dataStructures/trie)
8889
* [B-Tree](src/main/java/dataStructures/bTree)
90+
* [Segment Tree](src/main/java/dataStructures/segmentTree) (Not covered in CS2040s but useful!)
8991
* Red-Black Tree (Not covered in CS2040s but useful!)
9092
* [Orthogonal Range Searching](src/main/java/algorithms/orthogonalRangeSearching)
9193
* Interval Trees (**WIP**)

docs/assets/images/SegmentTree.png

28.9 KB
Loading

src/main/java/dataStructures/heap/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,20 @@ That said, in practice, the array-based implementation of a heap often provides
2727
former, in cache efficiency and memory locality. This is due to its contiguous memory layout. As such,
2828
the implementation shown here is a 0-indexed array-based heap.
2929

30+
#### Obtain index representing child nodes
31+
Suppose the parent node is captured at index *i* of the array (1-indexed).
32+
**1-indexed**: <br>
33+
Left Child: *i* x 2 <br>
34+
Right Child: *i* x 2 + 1 <br>
35+
36+
The 1-indexed calculation is intuitive. So, when dealing with 0-indexed representation (as in our implementation),
37+
one option is to convert 0-indexed to 1-indexed representation, do the above calculations, and revert. <br>
38+
(Note: Now, we assume parent node is captured at index *i* (0-indexed))
39+
40+
**0-indexed**: <br>
41+
Left Child: (*i* + 1) x 2 - 1 = *i* x 2 + 1 <br>
42+
Right Child: (*i* + 1) x 2 + 1 - 1 = *i* x 2 + 2 <br>
43+
3044
### Relevance of increaseKey and decreaseKey operations
3145

3246
The decision not to include explicit "decrease key" and "increase key" operations in the standard implementations of
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Segment Tree
2+
3+
## Background
4+
Segment Trees are primarily used to solve problems that require answers to queries on intervals of an array
5+
with the possibility of modifying the array elements.
6+
These queries could be finding the sum, minimum, or maximum in a subarray, or similar aggregated results.
7+
8+
![Segment Tree](../../../../../docs/assets/images/SegmentTree.png)
9+
10+
### Structure
11+
(Note: See below for a brief description of the array-based implementation of a segment tree)
12+
13+
A Segment Tree for an array of size *n* is a binary tree that stores information about segments of the array.
14+
Each node in the tree represents an interval of the array, with the root representing the entire array.
15+
The structure satisfies the following properties:
16+
1. Leaf Nodes: Each leaf node represents a single element of the array.
17+
2. Internal Nodes: Each internal node represents the sum of the values of its children
18+
(which captures the segment of the array). Summing up, this node captures the whole segment.
19+
3. Height: The height of the Segment Tree is O(log *n*), making queries and updates efficient.
20+
21+
## Complexity Analysis
22+
**Time**: O(log(n)) in general for query and update operations,
23+
except construction which takes O(nlogn)
24+
25+
**Space**: O(n), note for an array-based implementation, the array created should have size 4n (explained later)
26+
27+
where n is the number of elements in the array.
28+
29+
## Operations
30+
### Construction
31+
The construction of a Segment Tree starts with the root node representing the entire array and
32+
recursively dividing the array into two halves until each segment is reduced to a single element.
33+
This process is a divide-and-conquer strategy:
34+
1. Base Case: If the current segment of the array is reduced to a single element, create a leaf node.
35+
2. Recursive Case: Otherwise, split the array segment into two halves, construct the left and right children,
36+
and then merge their results to build the parent node.
37+
38+
This takes O(nlogn). logn in depth, and will visit each leaf node (number of leaf nodes could be roughly 2n) once.
39+
40+
### Querying
41+
To query an interval, say to find the sum of elements in the interval (L, R),
42+
the tree is traversed starting from the root:
43+
1. If the current node's segment is completely within (L, R), its value is part of the answer.
44+
2. If the current node's segment is completely outside (L, R), it is ignored.
45+
3. If the current node's segment partially overlaps with (L, R), the query is recursively applied to its children.
46+
47+
This approach ensures that each level of the tree is visited only once, time complexity of O(logn).
48+
49+
### Updating
50+
Updating an element involves changing the value of a leaf node and then propagating this change up to the root
51+
to ensure the tree reflects the updated array.
52+
This is done by traversing the path from the leaf node to the root
53+
and updating each node along this path (update parent to the sum of its children).
54+
55+
This can be done in O(logn).
56+
57+
## Array-based Segment Tree
58+
The array-based implementation of a Segment Tree is an efficient way to represent the tree in memory, especially
59+
since a Segment Tree is a complete binary tree.
60+
This method utilizes a simple array where each element of the array corresponds to a node in the tree,
61+
including both leaves and internal nodes.
62+
63+
### Why 4n space
64+
The size of the array needed to represent a Segment Tree for an array of size *n* is 2*2^ceil(log2(*n*)) - 1.
65+
We do 2^(ceil(log2(*n*))) because *n* might not be a perfect power of 2,
66+
**so we expand the array size to the next power of 2**.
67+
This adjustment ensures that each level of the tree is fully filled except possibly for the last level,
68+
which is filled from left to right.
69+
70+
**BUT**, 2^(ceil(log2(*n*))) seems overly-complex. To ensure we have sufficient space, we can just consider 2*n
71+
because 2*n >= 2^(ceil(log2(*n*))).
72+
Now, these 2n nodes can be thought of as the 'leaf' nodes (or more precisely, an upper-bound). To account for the
73+
intermediate nodes, we use the property that for a complete binary that is fully filled, the number of leaf nodes
74+
= number of intermediate nodes (recall: sum i -> 0 to n-1 of 2^i = 2^n). So we create an array of size 2n * 2 = 4n to
75+
guarantee we can house the entire segment tree.
76+
77+
### Obtain index representing child nodes
78+
Suppose the parent node is captured at index *i* of the array (1-indexed).
79+
**1-indexed**: <br>
80+
Left Child: *i* x 2 <br>
81+
Right Child: *i* x 2 + 1 <br>
82+
83+
The 1-indexed calculation is intuitive. So, when dealing with 0-indexed representation (as in our implementation),
84+
one option is to convert 0-indexed to 1-indexed representation, do the above calculations, and revert. <br>
85+
(Note: Now, we assume parent node is captured at index *i* (0-indexed))
86+
87+
**0-indexed**: <br>
88+
Left Child: (*i* + 1) x 2 - 1 = *i* x 2 + 1 <br>
89+
Right Child: (*i* + 1) x 2 + 1 - 1 = *i* x 2 + 2 <br>
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package dataStructures.segmentTree;
2+
3+
/**
4+
* Implementation of a Segment Tree. Uses SegmentTreeNode as a helper node class.
5+
*/
6+
public class SegmentTree {
7+
private SegmentTreeNode root;
8+
private int[] array;
9+
10+
/**
11+
* Helper node class. Used internally.
12+
*/
13+
private class SegmentTreeNode {
14+
private SegmentTreeNode leftChild; // left child
15+
private SegmentTreeNode rightChild; // right child
16+
private int start; // start idx of range captured
17+
private int end; // end idx of range captured
18+
private int sum; // sum of all elements between start and end index inclusive
19+
20+
/**
21+
* Constructor
22+
* @param leftChild
23+
* @param rightChild
24+
* @param start
25+
* @param end
26+
* @param sum
27+
*/
28+
public SegmentTreeNode(SegmentTreeNode leftChild, SegmentTreeNode rightChild, int start, int end, int sum) {
29+
this.leftChild = leftChild;
30+
this.rightChild = rightChild;
31+
this.start = start;
32+
this.end = end;
33+
this.sum = sum;
34+
}
35+
}
36+
37+
/**
38+
* Constructor.
39+
* @param nums
40+
*/
41+
public SegmentTree(int[] nums) {
42+
root = buildTree(nums, 0, nums.length - 1);
43+
array = nums;
44+
}
45+
46+
private SegmentTreeNode buildTree(int[] nums, int start, int end) {
47+
if (start == end) {
48+
return new SegmentTreeNode(null, null, start, end, nums[start]);
49+
}
50+
int mid = start + (end - start) / 2;
51+
SegmentTreeNode left = buildTree(nums, start, mid);
52+
SegmentTreeNode right = buildTree(nums, mid + 1, end);
53+
return new SegmentTreeNode(left, right, start, end, left.sum + right.sum);
54+
}
55+
56+
/**
57+
* Queries the sum of all values in the specified range.
58+
* @param leftEnd
59+
* @param rightEnd
60+
* @return the sum.
61+
*/
62+
public int query(int leftEnd, int rightEnd) {
63+
return query(root, leftEnd, rightEnd);
64+
}
65+
66+
private int query(SegmentTreeNode node, int leftEnd, int rightEnd) {
67+
// this is the case when:
68+
// start end
69+
// range query: ^ ^ --> so simply capture the sum at this node!
70+
if (leftEnd <= node.start && node.end <= rightEnd) {
71+
return node.sum;
72+
}
73+
int rangeSum = 0;
74+
int mid = node.start + (node.end - node.start) / 2;
75+
// Consider the 3 possible kinds of range queries
76+
// start mid end
77+
// poss 1: ^ ^
78+
// poss 2: ^ ^
79+
// poss 3: ^ ^
80+
if (leftEnd <= mid) {
81+
rangeSum += query(node.leftChild, leftEnd, Math.min(rightEnd, mid)); // poss1 or poss2
82+
}
83+
if (mid + 1 <= rightEnd) {
84+
rangeSum += query(node.rightChild, Math.max(leftEnd, mid + 1), rightEnd); // poss2 or poss3
85+
}
86+
return rangeSum;
87+
}
88+
89+
/**
90+
* Updates the segment tree based on updates to the array at the specified index with the specified value.
91+
* @param idx
92+
* @param val
93+
*/
94+
public void update(int idx, int val) {
95+
if (idx > array.length) {
96+
return;
97+
}
98+
array[idx] = val;
99+
update(root, idx, val);
100+
}
101+
102+
private void update(SegmentTreeNode node, int idx, int val) {
103+
if (node.start == node.end && node.start == idx) {
104+
node.sum = val; // node is holding a single value; now updated
105+
return;
106+
}
107+
int mid = node.start + (node.end - node.start) / 2;
108+
if (idx <= mid) {
109+
update(node.leftChild, idx, val);
110+
} else {
111+
update(node.rightChild, idx, val);
112+
}
113+
node.sum = node.leftChild.sum + node.rightChild.sum; // propagate updates up
114+
}
115+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package dataStructures.segmentTree.arrayRepresentation;
2+
3+
/**
4+
* Array-based implementation of a Segment Tree.
5+
*/
6+
public class SegmentTree {
7+
private int[] tree;
8+
private int[] array;
9+
10+
/**
11+
* Constructor.
12+
* @param nums
13+
*/
14+
public SegmentTree(int[] nums) {
15+
tree = new int[4 * nums.length]; // Need to account for up to 4n nodes.
16+
array = nums;
17+
buildTree(nums, 0, nums.length - 1, 0);
18+
}
19+
20+
/**
21+
* Builds the tree from the given array of numbers.
22+
* Unlikely before where we capture child nodes in the helper node class, here we capture position of child nodes
23+
* in the array-representation of the tree with an additional variable.
24+
* @param nums
25+
* @param start
26+
* @param end
27+
* @param idx tells us which index of the tree array we are at.
28+
*/
29+
private void buildTree(int[] nums, int start, int end, int idx) {
30+
// recall, each node is a position in the array
31+
// explicitly track which position in the array to fill with idx variable
32+
if (start == end) {
33+
tree[idx] = nums[start];
34+
return;
35+
}
36+
int mid = start + (end - start) / 2;
37+
int idxLeftChild = (idx + 1) * 2 - 1; // convert from 0-based to 1-based, do computation, then revert
38+
buildTree(nums, start, mid, idxLeftChild);
39+
int idxRightChild = (idx + 1) * 2 + 1 - 1; // convert from 0-based to 1-based, do computation, then revert
40+
buildTree(nums, mid + 1, end, idxRightChild);
41+
tree[idx] = tree[idxLeftChild] + tree[idxRightChild];
42+
}
43+
44+
/**
45+
* Queries the sum of all values in the specified range.
46+
* @param leftEnd
47+
* @param rightEnd
48+
* @return the sum.
49+
*/
50+
public int query(int leftEnd, int rightEnd) {
51+
return query(0, 0, array.length - 1, leftEnd, rightEnd);
52+
}
53+
54+
private int query(int nodeIdx, int startRange, int endRange, int leftEnd, int rightEnd) {
55+
// this is the case when:
56+
// start end
57+
// range query: ^ ^ --> so simply capture the sum at this node!
58+
if (leftEnd <= startRange && endRange <= rightEnd) {
59+
return tree[nodeIdx];
60+
}
61+
int rangeSum = 0;
62+
int mid = startRange + (endRange - startRange) / 2;
63+
// Consider the 3 possible kinds of range queries
64+
// start mid end
65+
// poss 1: ^ ^
66+
// poss 2: ^ ^
67+
// poss 3: ^ ^
68+
if (leftEnd <= mid) {
69+
int idxLeftChild = (nodeIdx + 1) * 2 - 1;
70+
rangeSum += query(idxLeftChild, startRange, mid, leftEnd, Math.min(rightEnd, mid));
71+
}
72+
if (mid + 1 <= rightEnd) {
73+
int idxRightChild = (nodeIdx + 1) * 2 + 1 - 1;
74+
rangeSum += query(idxRightChild, mid + 1, endRange, Math.max(leftEnd, mid + 1), rightEnd);
75+
}
76+
return rangeSum;
77+
}
78+
79+
/**
80+
* Updates the segment tree based on updates to the array at the specified index with the specified value.
81+
* @param idx
82+
* @param val
83+
*/
84+
public void update(int idx, int val) {
85+
if (idx > array.length) {
86+
return;
87+
}
88+
array[idx] = val;
89+
update(0, 0, array.length - 1, idx, val);
90+
}
91+
92+
private void update(int nodeIdx, int startRange, int endRange, int idx, int val) {
93+
if (startRange == endRange) {
94+
tree[nodeIdx] = val;
95+
return;
96+
}
97+
int mid = startRange + (endRange - startRange) / 2;
98+
if (idx <= mid) {
99+
update(nodeIdx * 2 + 1, startRange, mid, idx, val);
100+
} else {
101+
update(nodeIdx * 2 + 2, mid + 1, endRange, idx, val);
102+
}
103+
tree[nodeIdx] = tree[nodeIdx * 2 + 1] + tree[nodeIdx * 2 + 2];
104+
}
105+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package dataStructures.segmentTree;
2+
import static org.junit.Assert.assertEquals;
3+
4+
import org.junit.Test;
5+
6+
public class SegmentTreeTest {
7+
@Test
8+
public void construct_shouldConstructSegmentTree() {
9+
int[] arr1 = new int[] {7, 77, 37, 67, 33, 73, 13, 2, 7, 17, 87, 53};
10+
SegmentTree tree1 = new SegmentTree(arr1);
11+
assertEquals(arr1[1] + arr1[2] + arr1[3], tree1.query(1, 3));
12+
assertEquals(arr1[4] + arr1[5] + arr1[6] + arr1[7], tree1.query(4, 7));
13+
int sum1 = 0;
14+
for (int i = 0; i < arr1.length; i++) {
15+
sum1 += arr1[i];
16+
}
17+
assertEquals(sum1, tree1.query(0, arr1.length - 1));
18+
19+
20+
int[] arr2 = new int[] {7, -77, 37, 67, -33, 0, 73, -13, 2, -7, 17, 0, -87, 53, 0}; // some negatives and 0s
21+
SegmentTree tree2 = new SegmentTree(arr1);
22+
assertEquals(arr1[1] + arr1[2] + arr1[3], tree2.query(1, 3));
23+
assertEquals(arr1[4] + arr1[5] + arr1[6] + arr1[7], tree2.query(4, 7));
24+
int sum2 = 0;
25+
for (int i = 0; i < arr1.length; i++) {
26+
sum2 += arr1[i];
27+
}
28+
assertEquals(sum2, tree2.query(0, arr1.length - 1));
29+
}
30+
31+
@Test
32+
public void update_shouldUpdateSegmentTree() {
33+
int[] arr = new int[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
34+
SegmentTree tree = new SegmentTree(arr);
35+
assertEquals(55, tree.query(0, 10));
36+
tree.update(5, 55);
37+
assertEquals(105, tree.query(0, 10));
38+
}
39+
}

0 commit comments

Comments
 (0)