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 <bbj1979@gmail.com>

* Update DataStructures/DisjointSets/DisjointSetNode.php

Co-authored-by: Brandon Johnson <bbj1979@gmail.com>

* Update DataStructures/DisjointSets/DisjointSetNode.php

Co-authored-by: Brandon Johnson <bbj1979@gmail.com>

* Update tests/DataStructures/DisjointSetTest.php

Co-authored-by: Brandon Johnson <bbj1979@gmail.com>

* Update tests/DataStructures/DisjointSetTest.php

Co-authored-by: Brandon Johnson <bbj1979@gmail.com>

* Update tests/DataStructures/DisjointSetTest.php

Co-authored-by: Brandon Johnson <bbj1979@gmail.com>

* 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 <bbj1979@gmail.com>
Co-authored-by: Ramy-Badr-Ahmed <Ramy-Badr-Ahmed@users.noreply.github.com>
This commit is contained in:
Ramy 2024-09-18 07:07:12 +02:00 committed by GitHub
parent 8c808cd2e6
commit e43b4bfec8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 726 additions and 0 deletions

View File

@ -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)

View File

@ -0,0 +1,306 @@
<?php
namespace DataStructures\AVLTree;
/**
* Class AVLTree
* Implements an AVL Tree data structure with self-balancing capability.
*/
class AVLTree
{
private ?AVLTreeNode $root;
private int $counter;
public function __construct()
{
$this->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;
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace DataStructures\AVLTree;
class AVLTreeNode
{
/**
* @var int|string
*/
public $key;
/**
* @var mixed
*/
public $value;
public ?AVLTreeNode $left;
public ?AVLTreeNode $right;
public int $height;
public function __construct($key, $value, ?AVLTreeNode $left = null, ?AVLTreeNode $right = null)
{
$this->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;
}
}

View File

@ -0,0 +1,80 @@
<?php
namespace DataStructures\AVLTree;
abstract class TreeTraversal
{
/**
* Perform an in-order traversal of the subtree.
* Recursively traverses the subtree rooted at the given node.
*/
public static function inOrder(?AVLTreeNode $node): array
{
$result = [];
if ($node !== null) {
$result = array_merge($result, self::inOrder($node->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;
}
}

View File

@ -0,0 +1,295 @@
<?php
namespace DataStructures;
require_once __DIR__ . '/../../DataStructures/AVLTree/AVLTree.php';
require_once __DIR__ . '/../../DataStructures/AVLTree/AVLTreeNode.php';
require_once __DIR__ . '/../../DataStructures/AVLTree/TreeTraversal.php';
use DataStructures\AVLTree\AVLTree;
use DataStructures\AVLTree\TreeTraversal;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
use ReflectionException;
class AVLTreeTest extends TestCase
{
private AVLTree $tree;
protected function setUp(): void
{
$this->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');
}
}