From e43b4bfec82ad976db3d2507fe20f94259a30276 Mon Sep 17 00:00:00 2001 From: Ramy <126559907+Ramy-Badr-Ahmed@users.noreply.github.com> Date: Wed, 18 Sep 2024 07:07:12 +0200 Subject: [PATCH] Implemented AVL Tree Data Structure (#163) * Added Disjoint Sets Data structure * Moved DisjointSetTest.php to tests/DataStructures * Update DataStructures/DisjointSets/DisjointSet.php Co-authored-by: Brandon Johnson * Update DataStructures/DisjointSets/DisjointSetNode.php Co-authored-by: Brandon Johnson * Update DataStructures/DisjointSets/DisjointSetNode.php Co-authored-by: Brandon Johnson * Update tests/DataStructures/DisjointSetTest.php Co-authored-by: Brandon Johnson * Update tests/DataStructures/DisjointSetTest.php Co-authored-by: Brandon Johnson * Update tests/DataStructures/DisjointSetTest.php Co-authored-by: Brandon Johnson * Considered PHPCS remarks. Unit Testing is now working. * Remove data type mixed. Considered annotations for php7.4. * Remove data type mixed. Considered annotations for php7.4. * updating DIRECTORY.md * Implemented AVLTree DataStructure * Implemented AVLTree DataStructure --------- Co-authored-by: Brandon Johnson Co-authored-by: Ramy-Badr-Ahmed --- DIRECTORY.md | 4 + DataStructures/AVLTree/AVLTree.php | 306 +++++++++++++++++++++++ DataStructures/AVLTree/AVLTreeNode.php | 41 +++ DataStructures/AVLTree/TreeTraversal.php | 80 ++++++ tests/DataStructures/AVLTreeTest.php | 295 ++++++++++++++++++++++ 5 files changed, 726 insertions(+) create mode 100644 DataStructures/AVLTree/AVLTree.php create mode 100644 DataStructures/AVLTree/AVLTreeNode.php create mode 100644 DataStructures/AVLTree/TreeTraversal.php create mode 100644 tests/DataStructures/AVLTreeTest.php diff --git a/DIRECTORY.md b/DIRECTORY.md index 7be3da43..a1641443 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -17,6 +17,9 @@ * [Speedconversion](./Conversions/SpeedConversion.php) ## Datastructures + * AVLTree + * [AVLTree](./DataStructures/AVLTree/AVLTree.php) + * [AVLTreeNode](./DataStructures/AVLTree/AVLTreeNode.php) * Disjointsets * [Disjointset](./DataStructures/DisjointSets/DisjointSet.php) * [Disjointsetnode](./DataStructures/DisjointSets/DisjointSetNode.php) @@ -117,6 +120,7 @@ * Conversions * [Conversionstest](./tests/Conversions/ConversionsTest.php) * Datastructures + * [AVLTreeTest](./tests/DataStructures/AVLTreeTest.php) * [Disjointsettest](./tests/DataStructures/DisjointSetTest.php) * [Doublylinkedlisttest](./tests/DataStructures/DoublyLinkedListTest.php) * [Queuetest](./tests/DataStructures/QueueTest.php) diff --git a/DataStructures/AVLTree/AVLTree.php b/DataStructures/AVLTree/AVLTree.php new file mode 100644 index 00000000..a93b5f32 --- /dev/null +++ b/DataStructures/AVLTree/AVLTree.php @@ -0,0 +1,306 @@ +root = null; + $this->counter = 0; + } + + /** + * Get the root node of the AVL Tree. + */ + public function getRoot(): ?AVLTreeNode + { + return $this->root; + } + + /** + * Retrieve a node by its key. + * + * @param mixed $key The key of the node to retrieve. + * @return ?AVLTreeNode The node with the specified key, or null if not found. + */ + public function getNode($key): ?AVLTreeNode + { + return $this->searchNode($this->root, $key); + } + + /** + * Get the number of nodes in the AVL Tree. + */ + public function size(): int + { + return $this->counter; + } + + /** + * Insert a key-value pair into the AVL Tree. + * + * @param mixed $key The key to insert. + * @param mixed $value The value associated with the key. + */ + public function insert($key, $value): void + { + $this->root = $this->insertNode($this->root, $key, $value); + $this->counter++; + } + + /** + * Delete a node by its key from the AVL Tree. + * + * @param mixed $key The key of the node to delete. + */ + public function delete($key): void + { + $this->root = $this->deleteNode($this->root, $key); + $this->counter--; + } + + /** + * Search for a value by its key. + * + * @param mixed $key The key to search for. + * @return mixed The value associated with the key, or null if not found. + */ + public function search($key) + { + $node = $this->searchNode($this->root, $key); + return $node ? $node->value : null; + } + + /** + * Perform an in-order traversal of the AVL Tree. + * Initiates the traversal on the root node directly and returns the array of key-value pairs. + */ + public function inOrderTraversal(): array + { + return TreeTraversal::inOrder($this->root); + } + + /** + * Perform a pre-order traversal of the AVL Tree. + * Initiates the traversal on the root node directly and returns the array of key-value pairs. + */ + public function preOrderTraversal(): array + { + return TreeTraversal::preOrder($this->root); + } + + /** + * Perform a post-order traversal of the AVL Tree. + * Initiates the traversal on the root node directly and returns the array of key-value pairs. + */ + public function postOrderTraversal(): array + { + return TreeTraversal::postOrder($this->root); + } + + /** + * Perform a breadth-first traversal of the AVL Tree. + */ + public function breadthFirstTraversal(): array + { + return TreeTraversal::breadthFirst($this->root); + } + + /** + * Check if the AVL Tree is balanced. + * This method check balance starting from the root node directly + */ + public function isBalanced(): bool + { + return $this->isBalancedHelper($this->root); + } + + /** + * Insert a node into the AVL Tree and balance the tree. + * + * @param ?AVLTreeNode $node The current node. + * @param mixed $key The key to insert. + * @param mixed $value The value to insert. + * @return AVLTreeNode The new root of the subtree. + */ + private function insertNode(?AVLTreeNode $node, $key, $value): AVLTreeNode + { + if ($node === null) { + return new AVLTreeNode($key, $value); + } + + if ($key < $node->key) { + $node->left = $this->insertNode($node->left, $key, $value); + } elseif ($key > $node->key) { + $node->right = $this->insertNode($node->right, $key, $value); + } else { + $node->value = $value; // Update existing value + } + + $node->updateHeight(); + return $this->balance($node); + } + + /** + * Delete a node by its key and balance the tree. + * + * @param ?AVLTreeNode $node The current node. + * @param mixed $key The key of the node to delete. + * @return ?AVLTreeNode The new root of the subtree. + */ + private function deleteNode(?AVLTreeNode $node, $key): ?AVLTreeNode + { + if ($node === null) { + return null; + } + + if ($key < $node->key) { + $node->left = $this->deleteNode($node->left, $key); + } elseif ($key > $node->key) { + $node->right = $this->deleteNode($node->right, $key); + } else { + if (!$node->left) { + return $node->right; + } + if (!$node->right) { + return $node->left; + } + + $minNode = $this->getMinNode($node->right); + $node->key = $minNode->key; + $node->value = $minNode->value; + $node->right = $this->deleteNode($node->right, $minNode->key); + } + + $node->updateHeight(); + return $this->balance($node); + } + + /** + * Search for a node by its key. + * + * @param ?AVLTreeNode $node The current node. + * @param mixed $key The key to search for. + * @return ?AVLTreeNode The node with the specified key, or null if not found. + */ + private function searchNode(?AVLTreeNode $node, $key): ?AVLTreeNode + { + if ($node === null) { + return null; + } + + if ($key < $node->key) { + return $this->searchNode($node->left, $key); + } elseif ($key > $node->key) { + return $this->searchNode($node->right, $key); + } else { + return $node; + } + } + + /** + * Helper method to check if a subtree is balanced. + * + * @param ?AVLTreeNode $node The current node. + * @return bool True if the subtree is balanced, false otherwise. + */ + private function isBalancedHelper(?AVLTreeNode $node): bool + { + if ($node === null) { + return true; + } + + $leftHeight = $node->left ? $node->left->height : 0; + $rightHeight = $node->right ? $node->right->height : 0; + + $balanceFactor = abs($leftHeight - $rightHeight); + if ($balanceFactor > 1) { + return false; + } + + return $this->isBalancedHelper($node->left) && $this->isBalancedHelper($node->right); + } + + /** + * Balance the subtree rooted at the given node. + * + * @param ?AVLTreeNode $node The current node. + * @return ?AVLTreeNode The new root of the subtree. + */ + private function balance(?AVLTreeNode $node): ?AVLTreeNode + { + if ($node->balanceFactor() > 1) { + if ($node->left && $node->left->balanceFactor() < 0) { + $node->left = $this->rotateLeft($node->left); + } + return $this->rotateRight($node); + } + + if ($node->balanceFactor() < -1) { + if ($node->right && $node->right->balanceFactor() > 0) { + $node->right = $this->rotateRight($node->right); + } + return $this->rotateLeft($node); + } + + return $node; + } + + /** + * Perform a left rotation on the given node. + * + * @param AVLTreeNode $node The node to rotate. + * @return AVLTreeNode The new root of the rotated subtree. + */ + private function rotateLeft(AVLTreeNode $node): AVLTreeNode + { + $newRoot = $node->right; + $node->right = $newRoot->left; + $newRoot->left = $node; + + $node->updateHeight(); + $newRoot->updateHeight(); + + return $newRoot; + } + + /** + * Perform a right rotation on the given node. + * + * @param AVLTreeNode $node The node to rotate. + * @return AVLTreeNode The new root of the rotated subtree. + */ + private function rotateRight(AVLTreeNode $node): AVLTreeNode + { + $newRoot = $node->left; + $node->left = $newRoot->right; + $newRoot->right = $node; + + $node->updateHeight(); + $newRoot->updateHeight(); + + return $newRoot; + } + + /** + * Get the node with the minimum key in the given subtree. + * + * @param AVLTreeNode $node The root of the subtree. + * @return AVLTreeNode The node with the minimum key. + */ + private function getMinNode(AVLTreeNode $node): AVLTreeNode + { + while ($node->left) { + $node = $node->left; + } + return $node; + } +} diff --git a/DataStructures/AVLTree/AVLTreeNode.php b/DataStructures/AVLTree/AVLTreeNode.php new file mode 100644 index 00000000..707bf97f --- /dev/null +++ b/DataStructures/AVLTree/AVLTreeNode.php @@ -0,0 +1,41 @@ +key = $key; + $this->value = $value; + $this->left = $left; + $this->right = $right; + $this->height = 1; // New node is initially at height 1 + } + + public function updateHeight(): void + { + $leftHeight = $this->left ? $this->left->height : 0; + $rightHeight = $this->right ? $this->right->height : 0; + $this->height = max($leftHeight, $rightHeight) + 1; + } + + public function balanceFactor(): int + { + $leftHeight = $this->left ? $this->left->height : 0; + $rightHeight = $this->right ? $this->right->height : 0; + return $leftHeight - $rightHeight; + } +} diff --git a/DataStructures/AVLTree/TreeTraversal.php b/DataStructures/AVLTree/TreeTraversal.php new file mode 100644 index 00000000..803a856a --- /dev/null +++ b/DataStructures/AVLTree/TreeTraversal.php @@ -0,0 +1,80 @@ +left)); + $result[] = [$node->key => $node->value]; + $result = array_merge($result, self::inOrder($node->right)); + } + return $result; + } + + /** + * Perform a pre-order traversal of the subtree. + * Recursively traverses the subtree rooted at the given node. + */ + public static function preOrder(?AVLTreeNode $node): array + { + $result = []; + if ($node !== null) { + $result[] = [$node->key => $node->value]; + $result = array_merge($result, self::preOrder($node->left)); + $result = array_merge($result, self::preOrder($node->right)); + } + return $result; + } + + /** + * Perform a post-order traversal of the subtree. + * Recursively traverses the subtree rooted at the given node. + */ + public static function postOrder(?AVLTreeNode $node): array + { + $result = []; + if ($node !== null) { + $result = array_merge($result, self::postOrder($node->left)); + $result = array_merge($result, self::postOrder($node->right)); + $result[] = [$node->key => $node->value]; + } + return $result; + } + + /** + * Perform a breadth-first traversal of the AVL Tree. + */ + public static function breadthFirst(?AVLTreeNode $root): array + { + $result = []; + if ($root === null) { + return $result; + } + + $queue = []; + $queue[] = $root; + + while (!empty($queue)) { + $currentNode = array_shift($queue); + $result[] = [$currentNode->key => $currentNode->value]; + + if ($currentNode->left !== null) { + $queue[] = $currentNode->left; + } + + if ($currentNode->right !== null) { + $queue[] = $currentNode->right; + } + } + + return $result; + } +} diff --git a/tests/DataStructures/AVLTreeTest.php b/tests/DataStructures/AVLTreeTest.php new file mode 100644 index 00000000..8f556de8 --- /dev/null +++ b/tests/DataStructures/AVLTreeTest.php @@ -0,0 +1,295 @@ +tree = new AVLTree(); + } + + private function populateTree(): void + { + $this->tree->insert(10, 'Value 10'); + $this->tree->insert(20, 'Value 20'); + $this->tree->insert(5, 'Value 5'); + $this->tree->insert(15, 'Value 15'); + } + + public function testInsertAndSearch(): void + { + $this->populateTree(); + + $this->assertEquals('Value 10', $this->tree->search(10), 'Value for key 10 should be "Value 10"'); + $this->assertEquals('Value 20', $this->tree->search(20), 'Value for key 20 should be "Value 20"'); + $this->assertEquals('Value 5', $this->tree->search(5), 'Value for key 5 should be "Value 5"'); + $this->assertNull($this->tree->search(25), 'Value for non-existent key 25 should be null'); + } + + public function testDelete(): void + { + $this->populateTree(); + + $this->tree->delete(20); + $this->tree->delete(5); + + $this->assertNull($this->tree->search(20), 'Value for deleted key 20 should be null'); + $this->assertNull($this->tree->search(5), 'Value for deleted key 5 should be null'); + + $this->tree->delete(50); + + $this->assertNotNull($this->tree->search(10), 'Value for key 10 should still exist'); + $this->assertNotNull($this->tree->search(15), 'Value for key 15 should still exist'); + $this->assertNull($this->tree->search(50), 'Value for non-existent key 50 should be null'); + + $expectedInOrderAfterDelete = [ + [10 => 'Value 10'], + [15 => 'Value 15'] + ]; + + $result = TreeTraversal::inOrder($this->tree->getRoot()); + $this->assertEquals( + $expectedInOrderAfterDelete, + $result, + 'In-order traversal after deletion should match expected result' + ); + } + + public function testInOrderTraversal(): void + { + $this->populateTree(); + + $expectedInOrder = [ + [5 => 'Value 5'], + [10 => 'Value 10'], + [15 => 'Value 15'], + [20 => 'Value 20'] + ]; + + $result = $this->tree->inOrderTraversal(); + $this->assertEquals($expectedInOrder, $result, 'In-order traversal should match expected result'); + } + + public function testPreOrderTraversal(): void + { + $this->populateTree(); + + $expectedPreOrder = [ + [10 => 'Value 10'], + [5 => 'Value 5'], + [20 => 'Value 20'], + [15 => 'Value 15'] + ]; + + $result = $this->tree->preOrderTraversal(); + $this->assertEquals($expectedPreOrder, $result, 'Pre-order traversal should match expected result'); + } + + public function testPostOrderTraversal(): void + { + $this->populateTree(); + + $expectedPostOrder = [ + [5 => 'Value 5'], + [15 => 'Value 15'], + [20 => 'Value 20'], + [10 => 'Value 10'] + ]; + + $result = TreeTraversal::postOrder($this->tree->getRoot()); + $this->assertEquals($expectedPostOrder, $result, 'Post-order traversal should match expected result'); + } + + public function testBreadthFirstTraversal(): void + { + $this->populateTree(); + + $expectedBFT = [ + [10 => 'Value 10'], + [5 => 'Value 5'], + [20 => 'Value 20'], + [15 => 'Value 15'] + ]; + + $result = TreeTraversal::breadthFirst($this->tree->getRoot()); + $this->assertEquals($expectedBFT, $result, 'Breadth-first traversal should match expected result'); + } + + public function testInsertAndDeleteSingleNode(): void + { + $this->tree = new AVLTree(); + + $this->tree->insert(1, 'One'); + $this->assertEquals('One', $this->tree->search(1), 'Value for key 1 should be "One"'); + $this->tree->delete(1); + $this->assertNull($this->tree->search(1), 'Value for key 1 should be null after deletion'); + } + + public function testDeleteFromEmptyTree(): void + { + $this->tree = new AVLTree(); + + $this->tree->delete(1); + $this->assertNull($this->tree->search(1), 'Value for key 1 should be null as it was never inserted'); + } + + public function testInsertDuplicateKeys(): void + { + $this->tree = new AVLTree(); + + $this->tree->insert(1, 'One'); + $this->tree->insert(1, 'One Updated'); + $this->assertEquals( + 'One Updated', + $this->tree->search(1), + 'Value for key 1 should be "One Updated" after updating' + ); + } + + public function testLargeTree(): void + { + // Inserting a large number of nodes + for ($i = 1; $i <= 1000; $i++) { + $this->tree->insert($i, "Value $i"); + } + + // Verify that all inserted nodes can be searched + for ($i = 1; $i <= 1000; $i++) { + $this->assertEquals("Value $i", $this->tree->search($i), "Value for key $i should be 'Value $i'"); + } + + // Verify that all inserted nodes can be deleted + for ($i = 1; $i <= 5; $i++) { + $this->tree->delete($i); + $this->assertNull($this->tree->search($i), "Value for key $i should be null after deletion"); + } + } + + public function testBalance(): void + { + $this->populateTree(); + + // Perform operations that may unbalance the tree + $this->tree->insert(30, 'Value 30'); + $this->tree->insert(25, 'Value 25'); + + // After insertions, check the balance + $this->assertTrue($this->tree->isBalanced(), 'Tree should be balanced after insertions'); + } + + public function testRightRotation(): void + { + $this->populateTreeForRightRotation(); + + // Insert a node that will trigger a right rotation + $this->tree->insert(40, 'Value 40'); + + // Verify the tree structure after rotation + $root = $this->tree->getRoot(); + $this->assertEquals(20, $root->key, 'Root should be 20 after right rotation'); + $this->assertEquals(10, $root->left->key, 'Left child of root should be 10'); + $this->assertEquals(30, $root->right->key, 'Right child of root should be 30'); + } + + private function populateTreeForRightRotation(): void + { + // Insert nodes in a way that requires a right rotation + $this->tree->insert(10, 'Value 10'); + $this->tree->insert(20, 'Value 20'); + $this->tree->insert(30, 'Value 30'); // This should trigger a right rotation around 10 + } + + public function testLeftRotation(): void + { + $this->populateTreeForLeftRotation(); + + // Insert a node that will trigger a left rotation + $this->tree->insert(5, 'Value 5'); + + // Verify the tree structure after rotation + $root = $this->tree->getRoot(); + $this->assertEquals(20, $root->key, 'Root should be 20 after left rotation'); + $this->assertEquals(10, $root->left->key, 'Left child of root should be 10'); + $this->assertEquals(30, $root->right->key, 'Right child of root should be 30'); + } + + private function populateTreeForLeftRotation(): void + { + $this->tree->insert(30, 'Value 30'); + $this->tree->insert(20, 'Value 20'); + $this->tree->insert(10, 'Value 10'); // This should trigger a left rotation around 30 + } + + /** + * @throws ReflectionException + */ + public function testGetMinNode(): void + { + $this->populateTree(); + + // Using Reflection to access the private getMinNode method + $reflection = new ReflectionClass($this->tree); + $method = $reflection->getMethod('getMinNode'); + $method->setAccessible(true); + + $minNode = $method->invoke($this->tree, $this->tree->getRoot()); + + // Verify the minimum node + $this->assertEquals(5, $minNode->key, 'Minimum key in the tree should be 5'); + $this->assertEquals('Value 5', $minNode->value, 'Value for minimum key 5 should be "Value 5"'); + } + + public function testSizeAfterInsertions(): void + { + $this->tree = new AVLTree(); + + $this->assertEquals(0, $this->tree->size(), 'Size should be 0 initially'); + + $this->tree->insert(10, 'Value 10'); + $this->tree->insert(20, 'Value 20'); + $this->tree->insert(5, 'Value 5'); + + $this->assertEquals(3, $this->tree->size(), 'Size should be 3 after 3 insertions'); + + $this->tree->delete(20); + + $this->assertEquals(2, $this->tree->size(), 'Size should be 2 after deleting 1 node'); + } + + public function testSizeAfterMultipleInsertionsAndDeletions(): void + { + $this->tree = new AVLTree(); + + // Insert nodes + for ($i = 1; $i <= 10; $i++) { + $this->tree->insert($i, "Value $i"); + } + + $this->assertEquals(10, $this->tree->size(), 'Size should be 10 after 10 insertions'); + + for ($i = 1; $i <= 5; $i++) { + $this->tree->delete($i); + } + + $this->assertEquals(5, $this->tree->size(), 'Size should be 5 after deleting 5 nodes'); + } + + public function testSizeOnEmptyTree(): void + { + $this->tree = new AVLTree(); + $this->assertEquals(0, $this->tree->size(), 'Size should be 0 for an empty tree'); + } +}