diff --git a/DIRECTORY.md b/DIRECTORY.md index c1ece342..34964991 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -27,6 +27,9 @@ * [Doublylinkedlist](./DataStructures/DoublyLinkedList.php) * [Node](./DataStructures/Node.php) * [Queue](./DataStructures/Queue.php) + * SegmentTree + * [SegmentTree](./DataStructures/SegmentTree/SegmentTree.php) + * [SegmentTreeNode](./DataStructures/SegmentTree/SegmentTreeNode.php) * [Singlylinkedlist](./DataStructures/SinglyLinkedList.php) * [Stack](./DataStructures/Stack.php) * Trie @@ -125,6 +128,7 @@ * [Disjointsettest](./tests/DataStructures/DisjointSetTest.php) * [Doublylinkedlisttest](./tests/DataStructures/DoublyLinkedListTest.php) * [Queuetest](./tests/DataStructures/QueueTest.php) + * [SegmentTreeTest](./tests/DataStructures/SegmentTreeTest.php) * [Singlylinkedlisttest](./tests/DataStructures/SinglyLinkedListTest.php) * [Stacktest](./tests/DataStructures/StackTest.php) * [Trietest](./tests/DataStructures/TrieTest.php) diff --git a/DataStructures/AVLTree/AVLTree.php b/DataStructures/AVLTree/AVLTree.php index a93b5f32..ff05ff86 100644 --- a/DataStructures/AVLTree/AVLTree.php +++ b/DataStructures/AVLTree/AVLTree.php @@ -1,5 +1,13 @@ max($a, $b)); + * + * @param array $arr The input array for the segment tree + * @param callable|null $callback Optional callback function for custom aggregation logic. + * @throws InvalidArgumentException if the array is empty, contains non-numeric values, or is associative. + */ + public function __construct(array $arr, callable $callback = null) + { + $this->currentArray = $arr; + $this->arraySize = count($this->currentArray); + $this->callback = $callback; + + if ($this->isUnsupportedArray()) { + throw new InvalidArgumentException("Array must not be empty, must contain numeric values + and must be non-associative."); + } + + $this->root = $this->buildTree($this->currentArray, 0, $this->arraySize - 1); + } + + private function isUnsupportedArray(): bool + { + return empty($this->currentArray) || $this->isNonNumeric() || $this->isAssociative(); + } + + /** + * @return bool True if any element is non-numeric, false otherwise. + */ + private function isNonNumeric(): bool + { + return !array_reduce($this->currentArray, fn($carry, $item) => $carry && is_numeric($item), true); + } + + /** + * @return bool True if the array is associative, false otherwise. + */ + private function isAssociative(): bool + { + return array_keys($this->currentArray) !== range(0, $this->arraySize - 1); + } + + /** + * @return SegmentTreeNode The root node of the segment tree. + */ + public function getRoot(): SegmentTreeNode + { + return $this->root; + } + + /** + * @return array The original or the current array (after any update) stored in the segment tree. + */ + public function getCurrentArray(): array + { + return $this->currentArray; + } + + /** + * Builds the segment tree recursively. Takes O(n log n) in total. + * + * @param array $arr The input array. + * @param int $start The starting index of the segment. + * @param int $end The ending index of the segment. + * @return SegmentTreeNode The root node of the constructed segment. + */ + private function buildTree(array $arr, int $start, int $end): SegmentTreeNode + { + // Leaf node + if ($start == $end) { + return new SegmentTreeNode($start, $end, $arr[$start]); + } + + $mid = $start + (int)(($end - $start) / 2); + + // Recursively build left and right children + $leftChild = $this->buildTree($arr, $start, $mid); + $rightChild = $this->buildTree($arr, $mid + 1, $end); + + $node = new SegmentTreeNode($start, $end, $this->callback + ? ($this->callback)($leftChild->value, $rightChild->value) + : $leftChild->value + $rightChild->value); + + // Link the children to the parent node + $node->left = $leftChild; + $node->right = $rightChild; + + return $node; + } + + /** + * Queries the aggregated value over a specified range. Takes O(log n). + * + * @param int $start The starting index of the range. + * @param int $end The ending index of the range. + * @return int|float The aggregated value for the range. + * @throws OutOfBoundsException if the range is invalid. + */ + public function query(int $start, int $end) + { + if ($start > $end || $start < 0 || $end > ($this->root->end)) { + throw new OutOfBoundsException("Index out of bounds: start = $start, end = $end. + Must be between 0 and " . ($this->arraySize - 1)); + } + return $this->queryTree($this->root, $start, $end); + } + + /** + * Recursively queries the segment tree for a specific range. + * + * @param SegmentTreeNode $node The current node. + * @param int $start The starting index of the query range. + * @param int $end The ending index of the query range. + * @return int|float The aggregated value for the range. + */ + private function queryTree(SegmentTreeNode $node, int $start, int $end) + { + if ($node->start == $start && $node->end == $end) { + return $node->value; + } + + $mid = $node->start + (int)(($node->end - $node->start) / 2); + + // Determine which segment of the tree to query + if ($end <= $mid) { + return $this->queryTree($node->left, $start, $end); // Query left child + } elseif ($start > $mid) { + return $this->queryTree($node->right, $start, $end); // Query right child + } else { + // Split query between left and right children + $leftResult = $this->queryTree($node->left, $start, $mid); + $rightResult = $this->queryTree($node->right, $mid + 1, $end); + + return $this->callback + ? ($this->callback)($leftResult, $rightResult) + : $leftResult + $rightResult; // Default sum if no callback + } + } + + /** + * Updates the value at a specified index in the segment tree. Takes O(log n). + * + * @param int $index The index to update. + * @param int|float $value The new value to set. + * @throws OutOfBoundsException if the index is out of bounds. + */ + public function update(int $index, int $value): void + { + if ($index < 0 || $index >= $this->arraySize) { + throw new OutOfBoundsException("Index out of bounds: $index. Must be between 0 and " + . ($this->arraySize - 1)); + } + + $this->updateTree($this->root, $index, $value); + $this->currentArray[$index] = $value; // Reflect the update in the original array + } + + /** + * Recursively updates the segment tree. + * + * @param SegmentTreeNode $node The current node. + * @param int $index The index to update. + * @param int|float $value The new value. + */ + private function updateTree(SegmentTreeNode $node, int $index, $value): void + { + // Leaf node + if ($node->start == $node->end) { + $node->value = $value; + return; + } + + $mid = $node->start + (int)(($node->end - $node->start) / 2); + + // Decide whether to go to the left or right child + if ($index <= $mid) { + $this->updateTree($node->left, $index, $value); + } else { + $this->updateTree($node->right, $index, $value); + } + + // Recompute the value of the current node after the update + $node->value = $this->callback + ? ($this->callback)($node->left->value, $node->right->value) + : $node->left->value + $node->right->value; + } + + /** + * Performs a range update on a specified segment. + * + * @param int $start The starting index of the range. + * @param int $end The ending index of the range. + * @param int|float $value The value to set for the range. + * @throws OutOfBoundsException if the range is invalid. + */ + public function rangeUpdate(int $start, int $end, $value): void + { + if ($start < 0 || $end >= $this->arraySize || $start > $end) { + throw new OutOfBoundsException("Invalid range: start = $start, end = $end."); + } + $this->rangeUpdateTree($this->root, $start, $end, $value); + + // Update the original array to reflect the range update + $this->currentArray = array_replace($this->currentArray, array_fill_keys(range($start, $end), $value)); + } + + /** + * Recursively performs a range update in the segment tree. + * + * @param SegmentTreeNode $node The current node. + * @param int $start The starting index of the range. + * @param int $end The ending index of the range. + * @param int|float $value The new value for the range. + */ + private function rangeUpdateTree(SegmentTreeNode $node, int $start, int $end, $value): void + { + // Leaf node + if ($node->start == $node->end) { + $node->value = $value; + return; + } + + $mid = $node->start + (int)(($node->end - $node->start) / 2); + + // Determine which segment of the tree to update (Left, Right, Split respectively) + if ($end <= $mid) { + $this->rangeUpdateTree($node->left, $start, $end, $value); // Entire range is in the left child + } elseif ($start > $mid) { + $this->rangeUpdateTree($node->right, $start, $end, $value); // Entire range is in the right child + } else { + // Range is split between left and right children + $this->rangeUpdateTree($node->left, $start, $mid, $value); + $this->rangeUpdateTree($node->right, $mid + 1, $end, $value); + } + + // Recompute the value of the current node after the update + $node->value = $this->callback + ? ($this->callback)($node->left->value, $node->right->value) + : $node->left->value + $node->right->value; + } + + /** + * Serializes the segment tree into a JSON string. + * + * @return string The serialized segment tree as a JSON string. + */ + public function serialize(): string + { + return json_encode($this->serializeTree($this->root)); + } + + /** + * Recursively serializes the segment tree. + * + * @param SegmentTreeNode|null $node The current node. + * @return array The serialized representation of the node. + */ + private function serializeTree(?SegmentTreeNode $node): array + { + if ($node === null) { + return []; + } + return [ + 'start' => $node->start, + 'end' => $node->end, + 'value' => $node->value, + 'left' => $this->serializeTree($node->left), + 'right' => $this->serializeTree($node->right), + ]; + } + + /** + * Deserializes a JSON string into a SegmentTree object. + * + * @param string $data The JSON string to deserialize. + * @return SegmentTree The deserialized segment tree. + */ + public static function deserialize(string $data): self + { + $array = json_decode($data, true); + + $initialiseArray = array_fill(0, $array['end'] + 1, 0); + $segmentTree = new self($initialiseArray); + + $segmentTree->root = $segmentTree->deserializeTree($array); + return $segmentTree; + } + + /** + * Recursively deserializes a segment tree from an array representation. + * + * @param array $data The serialized data for the node. + * @return SegmentTreeNode|null The deserialized node. + */ + private function deserializeTree(array $data): ?SegmentTreeNode + { + if (empty($data)) { + return null; + } + $node = new SegmentTreeNode($data['start'], $data['end'], $data['value']); + + $node->left = $this->deserializeTree($data['left']); + $node->right = $this->deserializeTree($data['right']); + return $node; + } +} diff --git a/DataStructures/SegmentTree/SegmentTreeNode.php b/DataStructures/SegmentTree/SegmentTreeNode.php new file mode 100644 index 00000000..bb017c67 --- /dev/null +++ b/DataStructures/SegmentTree/SegmentTreeNode.php @@ -0,0 +1,38 @@ +start = $start; + $this->end = $end; + $this->value = $value; + $this->left = null; + $this->right = null; + } +} diff --git a/DataStructures/Trie/Trie.php b/DataStructures/Trie/Trie.php index 062d7f33..32c8c6d9 100644 --- a/DataStructures/Trie/Trie.php +++ b/DataStructures/Trie/Trie.php @@ -1,5 +1,13 @@ testArray = [1, 3, 5, 7, 9, 11, 13, 14, 15, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26]; + } + + public static function sumQueryProvider(): array + { + return [ + // Format: [expectedResult, startIndex, endIndex] + [24, 1, 4], + [107, 5, 11], + [91, 2, 9], + [23, 15, 15], + ]; + } + + /** + * Test sum queries using data provider. + * @dataProvider sumQueryProvider + */ + public function testSegmentTreeSumQuery(int $expected, int $startIndex, int $endIndex): void + { + // Test the default case: sum query + $segmentTree = new SegmentTree($this->testArray); + $this->assertEquals( + $expected, + $segmentTree->query($startIndex, $endIndex), + "Query sum between index $startIndex and $endIndex should return $expected." + ); + } + + public static function maxQueryProvider(): array + { + return [ + [26, 0, 18], + [13, 2, 6], + [22, 8, 14], + [11, 5, 5], + ]; + } + + /** + * Test max queries using data provider. + * @dataProvider maxQueryProvider + */ + public function testSegmentTreeMaxQuery(int $expected, int $startIndex, int $endIndex): void + { + $segmentTree = new SegmentTree($this->testArray, fn($a, $b) => max($a, $b)); + $this->assertEquals( + $expected, + $segmentTree->query($startIndex, $endIndex), + "Max query between index $startIndex and $endIndex should return $expected." + ); + } + + public static function minQueryProvider(): array + { + return [ + [1, 0, 18], + [5, 2, 7], + [18, 10, 17], + [17, 9, 9], + ]; + } + + /** + * Test min queries using data provider. + * @dataProvider minQueryProvider + */ + public function testSegmentTreeMinQuery(int $expected, int $startIndex, int $endIndex): void + { + $segmentTree = new SegmentTree($this->testArray, function ($a, $b) { + return min($a, $b); + }); + $this->assertEquals( + $expected, + $segmentTree->query($startIndex, $endIndex), + "Query min between index $startIndex and $endIndex should return $expected." + ); + } + + /** + * Test update functionality for different query types. + */ + public function testSegmentTreeUpdate(): void + { + // Sum Query + $segmentTreeSum = new SegmentTree($this->testArray); + $segmentTreeSum->update(2, 10); // Update index 2 to 10 + $this->assertEquals( + 29, + $segmentTreeSum->query(1, 4), + "After update, sum between index 1 and 4 should return 29." + ); + + // Max Query: with callback + $segmentTreeMax = new SegmentTree($this->testArray, fn($a, $b) => max($a, $b)); + $segmentTreeMax->update(12, -1); // Update index 12 to -1 + $this->assertEquals( + 19, + $segmentTreeMax->query(5, 12), + "After update, max between index 5 and 12 should return 19." + ); + + // Min Query: with callback + $segmentTreeMin = new SegmentTree($this->testArray, fn($a, $b) => min($a, $b)); + $segmentTreeMin->update(9, -5); // Update index 9 to -5 + $this->assertEquals( + -5, + $segmentTreeMin->query(9, 13), + "After update, min between index 9 and 13 should return -5." + ); + } + + /** + * Test range update functionality for different query types. + */ + public function testSegmentTreeRangeUpdate(): void + { + // Sum Query + $segmentTreeSum = new SegmentTree($this->testArray); + $segmentTreeSum->rangeUpdate(3, 7, 0); // Set indices 3 to 7 to 0 + $this->assertEquals( + 55, + $segmentTreeSum->query(2, 10), + "After range update, sum between index 2 and 10 should return 55." + ); + + // Max Query: with callback + $segmentTreeMax = new SegmentTree($this->testArray, fn($a, $b) => max($a, $b)); + $segmentTreeMax->rangeUpdate(3, 7, 0); // Set indices 3 to 7 to 0 + $this->assertEquals( + 5, + $segmentTreeMax->query(2, 7), + "After range update, max between index 2 and 7 should return 5." + ); + + // Min Query: with callback + $segmentTreeMin = new SegmentTree($this->testArray, fn($a, $b) => min($a, $b)); + $segmentTreeMin->rangeUpdate(3, 9, 0); // Set indices 3 to 9 to 0 + $this->assertEquals( + 0, + $segmentTreeMin->query(2, 9), + "After range update, min between index 2 and 9 should return 0." + ); + } + + /** + * Test array updates reflections. + */ + public function testGetCurrentArray(): void + { + $segmentTree = new SegmentTree($this->testArray); + + // Ensure the initial array matches the input array + $this->assertEquals( + $this->testArray, + $segmentTree->getCurrentArray(), + "getCurrentArray() should return the initial array." + ); + + // Perform an update and test again + $segmentTree->update(2, 10); // Update index 2 to 10 + $updatedArray = $this->testArray; + $updatedArray[2] = 10; + + $this->assertEquals( + $updatedArray, + $segmentTree->getCurrentArray(), + "getCurrentArray() should return the updated array." + ); + } + + /** + * Test serialization and deserialization of the segment tree. + */ + public function testSegmentTreeSerialization(): void + { + $segmentTree = new SegmentTree($this->testArray); + $serialized = $segmentTree->serialize(); + + $deserializedTree = SegmentTree::deserialize($serialized); + $this->assertEquals( + $segmentTree->query(1, 4), + $deserializedTree->query(1, 4), + "Serialized and deserialized trees should have the same query results." + ); + } + + /** + * Testing EdgeCases: first and last indices functionality on the Segment Tree + */ + public function testEdgeCases(): void + { + $segmentTree = new SegmentTree($this->testArray); + $firstIndex = 0; + $lastIndex = count($this->testArray) - 1; + + // Test querying the first and last indices + $this->assertEquals( + $this->testArray[$firstIndex], + $segmentTree->query($firstIndex, $firstIndex), + "Query at the first index should return {$this->testArray[$firstIndex]}." + ); + $this->assertEquals( + $this->testArray[$lastIndex], + $segmentTree->query($lastIndex, $lastIndex), + "Query at the last index should return {$this->testArray[$lastIndex]}." + ); + + + // Test updating the first index + $segmentTree->update($firstIndex, 100); // Update first index to 100 + $this->assertEquals( + 100, + $segmentTree->query($firstIndex, $firstIndex), + "After update, query at the first index should return {$this->testArray[$firstIndex]}." + ); + + // Test updating the last index + $segmentTree->update($lastIndex, 200); // Update last index to 200 + $this->assertEquals( + 200, + $segmentTree->query($lastIndex, $lastIndex), + "After update, query at the last index should return {$this->testArray[$lastIndex]}." + ); + + // Test range update that includes the first index + $segmentTree->rangeUpdate($firstIndex, 2, 50); // Set indices 0 to 2 to 50 + $this->assertEquals( + 50, + $segmentTree->query($firstIndex, $firstIndex), + "After range update, query at index $firstIndex should return 50." + ); + $this->assertEquals(50, $segmentTree->query(1, 1), "After range update, query at index 1 should return 50."); + $this->assertEquals(50, $segmentTree->query(2, 2), "After range update, query at index 2 should return 50."); + + // Test range update that includes the last index + $segmentTree->rangeUpdate($lastIndex - 3, $lastIndex, 10); // Set indices to 10 + $this->assertEquals( + 10, + $segmentTree->query($lastIndex, $lastIndex), + "After range update, query at the last index should return 10." + ); + $this->assertEquals( + 10, + $segmentTree->query(count($this->testArray) - 2, count($this->testArray) - 2), + "After range update, query at the second last index should return 10." + ); + } + + /** + * Test empty or unsupported arrays. + */ + public function testUnsupportedOrEmptyArrayInitialization(): void + { + // Test empty array + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Array must not be empty, must contain numeric values + and must be non-associative."); + + $segmentTreeEmpty = new SegmentTree([]); // expecting an exception + + // Test unsupported array (e.g., with non-numeric values) + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Array must not be empty, must contain numeric values + and must be non-associative."); + + $segmentTreeUnsupported = new SegmentTree([1, "two", 3]); // Mix of numeric and non-numeric + } + + + /** + * Test exception for invalid update index. + */ + public function testInvalidUpdateIndex(): void + { + $segmentTree = new SegmentTree($this->testArray); + + $index = count($this->testArray) + 5; + + // Expect an exception for range update with invalid indices + $this->expectException(OutOfBoundsException::class); + $this->expectExceptionMessage("Index out of bounds: $index. Must be between 0 and " + . (count($this->testArray) - 1)); + + $segmentTree->update($index, 100); // non-existing index, should trigger exception + } + + /** + * Test exception for invalid update index. + */ + public function testOutOfBoundsQuery(): void + { + $segmentTree = new SegmentTree($this->testArray); + + $start = 0; + $end = count($this->testArray); + + $this->expectException(OutOfBoundsException::class); + $this->expectExceptionMessage("Index out of bounds: start = $start, end = $end. + Must be between 0 and " . (count($this->testArray) - 1)); + + $segmentTree->query(0, count($this->testArray)); // expecting an exception + } + + /** + * Test exception for invalid range update. + */ + public function testInvalidRangeUpdate(): void + { + $segmentTree = new SegmentTree($this->testArray); + + $start = -1; + $end = 5; + + // Expect an exception for range update with invalid indices + $this->expectException(OutOfBoundsException::class); + $this->expectExceptionMessage("Invalid range: start = $start, end = $end."); + + $segmentTree->rangeUpdate(-1, 5, 0); // Negative index, should trigger exception + } +} diff --git a/tests/DataStructures/TrieTest.php b/tests/DataStructures/TrieTest.php index d2fa39d5..7733ed80 100644 --- a/tests/DataStructures/TrieTest.php +++ b/tests/DataStructures/TrieTest.php @@ -1,5 +1,13 @@