diff --git a/src/helper/Enum/Color.php b/src/helper/Enum/Color.php new file mode 100644 index 0000000..64f5f2e --- /dev/null +++ b/src/helper/Enum/Color.php @@ -0,0 +1,9 @@ +name; + } + + public function opposite(): Direction + { + return match ($this) { + Direction::Left => Direction::Right, + Direction::Right => Direction::Left, + }; + } +} diff --git a/src/helper/RedBlackNode.php b/src/helper/RedBlackNode.php new file mode 100644 index 0000000..8a91da7 --- /dev/null +++ b/src/helper/RedBlackNode.php @@ -0,0 +1,153 @@ +value; + } + + public function getParent(): ?RedBlackNode + { + return $this->parent; + } + + public function setParent(?RedBlackNode $node): void + { + $this->parent = $node; + } + + public function getColor(): Color + { + return $this->color; + } + + public function setColor(Color $color): void + { + $this->color = $color; + } + + public function getLeft(): ?RedBlackNode + { + return $this->left; + } + + public function setLeft(?RedBlackNode $node): void + { + $this->left = $node; + $node?->setParent($this); + } + + public function getRight(): ?RedBlackNode + { + return $this->right; + } + + public function setRight(?RedBlackNode $node): void + { + $this->right = $node; + $node?->setParent($this); + } + + /** + * Check if $this node is the child of its parents in $direction + * + * @param Direction $direction + * @return bool + */ + public function isChild(Direction $direction): bool + { + if (!$this->parent) { + return false; + } elseif ($this->parent->{$direction->operation('get')}() === $this) { + return true; + } + return false; + } + + /** + * Returns the sibling of the parent of $this + * + * @return RedBlackNode|null + */ + public function uncle(): ?RedBlackNode + { + if (!$this->parent || !$this->parent->getParent()) { + return null; + } + return $this->parent->sibling(); + } + + /** + * Return the opposite child of the parents to $this + * + * @return RedBlackNode|null + */ + public function sibling(): ?RedBlackNode + { + if (!$this->parent) { + return null; + } + return $this->isChild(Direction::Left) + ? $this->parent->getRight() + : $this->parent->getLeft(); + } + + /** + * Rotate the subtree around $this in the $direction and updates relations + * + * @param RedBlackTree $tree + * @param Direction $direction + * @return void + */ + public function rotate(RedBlackTree $tree, Direction $direction): void + { + if (!$this->{$direction->opposite()->value}) { + return; + } + $grandchild = $this->{$direction->opposite()->value}->{$direction->operation('get')}(); + $this->updateParent($tree, $direction->opposite()); + $this->{$direction->opposite()->value}->{$direction->operation('set')}($this); + $this->{$direction->opposite()->operation('set')}($grandchild); + } + + /** + * Update the parent of the node to the child in the $direction + * should $this be root (both children checks null), update parent to null and set child in $direction as new root + * + * @param RedBlackTree $tree + * @param Direction $direction + * @return void + */ + private function updateParent(RedBlackTree $tree, Direction $direction): void + { + if ($this->isChild(Direction::Left)) { + $this->parent->setLeft($this->{$direction->value}); + } elseif ($this->isChild(Direction::Right)) { + $this->parent->setRight($this->{$direction->value}); + } else { + $this->{$direction->value}->setParent(null); + $tree->setRoot($this->{$direction->value}); + } + } +} diff --git a/src/helper/RedBlackTree.php b/src/helper/RedBlackTree.php new file mode 100644 index 0000000..e6f068b --- /dev/null +++ b/src/helper/RedBlackTree.php @@ -0,0 +1,156 @@ +root; + } + + public function setRoot(RedBlackNode $node): void + { + $this->root = $node; + } + + /** + * Inserts a new node with provided $value into the tree + * + * @param int $value + * @return void + */ + public function insert(int $value): void + { + if ($this->root == null) { + $this->root = new RedBlackNode($value); + $this->root->setColor(Color::Black); + return; + } + $pointer = $this->binaryTreeInsert($this->root, $value); + do { + if ($pointer->getColor() == Color::Black) { + break; + } + if ($pointer->uncle() && $pointer->uncle()->getColor() == Color::Red) { + $pointer->getParent()->setColor(Color::Black); + $pointer->uncle()->setColor(Color::Black); + $pointer = $pointer->getParent()->getParent(); + continue; + } + if ($pointer->getParent()->isChild(Direction::Left) && $pointer->isChild(Direction::Left)) { + $pointer->getParent()->getParent()->rotate($this, Direction::Right); + $pointer->getParent()->setColor(Color::Black); + $pointer->sibling()->setColor(Color::Red); + continue; + } + if ($pointer->getParent()->isChild(Direction::Left) && $pointer->isChild(Direction::Right)) { + $pointer->getParent()->rotate($this, Direction::Left); + $pointer->getParent()->rotate($this, Direction::Right); + $pointer->setColor(Color::Black); + $pointer->getRight()->setColor(Color::Red); + continue; + } + if ($pointer->getParent()->isChild(Direction::Right) && $pointer->isChild(Direction::Right)) { + $pointer->getParent()->getParent()->rotate($this, Direction::Left); + $pointer->getParent()->setColor(Color::Black); + $pointer->sibling()->setColor(Color::Red); + continue; + } + if ($pointer->getParent()->isChild(Direction::Right) && $pointer->isChild(Direction::Left)) { + $pointer->getParent()->rotate($this, Direction::Right); + $pointer->getParent()->rotate($this, Direction::Left); + $pointer->setColor(Color::Black); + $pointer->getLeft()->setColor(Color::Red); + $pointer->getParent()->setColor(Color::Black); + continue; + } + break; + } while ($pointer); + } + + /** + * Delete the provided node from the tree + * + * @param RedBlackNode $node + * @return void + */ + public function delete(RedBlackNode $node): void + { + $this->binaryTreeDelete($node); + } + + /** + * BinaryTree insert of $value starting at $pointer + * + * @param RedBlackNode $pointer + * @param int $value + * @return RedBlackNode + */ + private function binaryTreeInsert(RedBlackNode $pointer, int $value): RedBlackNode + { + $node = new RedBlackNode($value); + do { + if ($value < $pointer->getValue()) { + if ($pointer->getLeft() == null) { + $pointer->setLeft($node); + return $node; + } + $pointer = $pointer->getLeft(); + } else { + if ($pointer->getRight() == null) { + $pointer->setRight($node); + return $node; + } + $pointer = $pointer->getRight(); + } + } while (true); + } + + /** + * BinaryTree deletion of $node + * + * @param RedBlackNode $node + * @return void + */ + private function binaryTreeDelete(RedBlackNode $node): void + { + if ($node === $this->root) { + $this->root = null; + return; + } + $parentChildDirection = $node->isChild(Direction::Left) + ? Direction::Left + : Direction::Right; + if (!$node->getRight() && !$node->getLeft()) { + $node->getParent()->{$parentChildDirection->operation('set')}(null); + } + if ($node->getLeft() && $node->getRight()) { + $pointer = $node->getRight(); + while ($pointer->getLeft()) { + $pointer = $pointer->getLeft(); + } + if ($pointer->getParent()->isChild(Direction::Left)) { + $pointer->getParent()->setLeft($pointer->getRight()); + } else { + $pointer->getParent()->setRight($pointer->getRight()); + } + $node->getParent()->{$parentChildDirection->operation('set')}($pointer); + $pointer->setRight($node->getRight()); + } + if ($node->getLeft() && !$node->getRight()) { + $node->getParent()->{$parentChildDirection->operation('set')}($node->getLeft()); + } + if ($node->getRight() && !$node->getLeft()) { + $node->getParent()->{$parentChildDirection->operation('set')}($node->getRight()); + } + } +} diff --git a/tests/RedBlackTreeTest.php b/tests/RedBlackTreeTest.php new file mode 100644 index 0000000..42dd029 --- /dev/null +++ b/tests/RedBlackTreeTest.php @@ -0,0 +1,332 @@ +insert(10); + $root = $tree->getRoot(); + + $this->assertNotNull($root); + $this->assertEquals(10, $root->getValue()); + $this->assertEquals(Color::Black, $root->getColor()); // Root must always be black + + $tree->insert(5); + $left = $root->getLeft(); + + $this->assertNotNull($left); + $this->assertEquals(5, $left->getValue()); + $this->assertEquals(Color::Red, $left->getColor()); + + $tree->insert(15); + $right = $root->getRight(); + + $this->assertNotNull($right); + $this->assertEquals(15, $right->getValue()); + $this->assertEquals(Color::Red, $right->getColor()); + } + + public function testInsertToTheRightTriggersRebalancing(): void + { + $tree = new RedBlackTree(); + + $tree->insert(10); + $tree->insert(20); + $tree->insert(30); + + $root = $tree->getRoot(); + + $this->assertEquals(20, $root->getValue()); + $this->assertEquals(Color::Black, $root->getColor()); + + $left = $root->getLeft(); + $right = $root->getRight(); + + $this->assertEquals(10, $left->getValue()); + $this->assertEquals(Color::Red, $left->getColor()); + + $this->assertEquals(30, $right->getValue()); + $this->assertEquals(Color::Red, $right->getColor()); + } + + public function testInsertToTheLeftTriggersRebalancing(): void + { + $tree = new RedBlackTree(); + + $tree->insert(30); + $tree->insert(20); + $tree->insert(10); + + $root = $tree->getRoot(); + + $this->assertEquals(20, $root->getValue()); + $this->assertEquals(Color::Black, $root->getColor()); + + $left = $root->getLeft(); + $right = $root->getRight(); + + $this->assertEquals(10, $left->getValue()); + $this->assertEquals(Color::Red, $left->getColor()); + + $this->assertEquals(30, $right->getValue()); + $this->assertEquals(Color::Red, $right->getColor()); + } + + public function testInsertHandlesNegativeValues(): void + { + $tree = new RedBlackTree(); + + $tree->insert(-10); + $tree->insert(-20); + $tree->insert(-5); + + $root = $tree->getRoot(); + + $this->assertEquals(-10, $root->getValue()); + $this->assertEquals(-20, $root->getLeft()->getValue()); + $this->assertEquals(-5, $root->getRight()->getValue()); + } + + public function testInsertHandlesLargeNumberOfValues(): void + { + $tree = new RedBlackTree(); + $values = range(1, 1000); + + foreach ($values as $value) { + $tree->insert($value); + } + + $root = $tree->getRoot(); + + $this->assertNotNull($root); + $this->assertTrue($this->isBalancedRedBlackTree($root)); + } + + public function testNodeGettersAndSetters(): void + { + $parent = new RedBlackNode(10); + $left = new RedBlackNode(5); + $right = new RedBlackNode(15); + + $node = new RedBlackNode(20, $parent); + $node->setLeft($left); + $node->setRight($right); + + $this->assertEquals(20, $node->getValue()); + $this->assertSame($parent, $node->getParent()); + $this->assertSame($left, $node->getLeft()); + $this->assertSame($right, $node->getRight()); + + $node->setColor(Color::Black); + $this->assertEquals(Color::Black, $node->getColor()); + + $node->setParent($left); + $this->assertSame($left, $node->getParent()); + } + + public function testIsChild(): void + { + $parent = new RedBlackNode(10); + $child = new RedBlackNode(5, $parent); + $parent->setLeft($child); + $this->assertTrue($child->isChild(Direction::Left)); + + $parent = new RedBlackNode(10); + $child = new RedBlackNode(15, $parent); + $parent->setRight($child); + $this->assertTrue($child->isChild(Direction::Right)); + } + + public function testUncle(): void + { + $grandparent = new RedBlackNode(20); + $parent = new RedBlackNode(10, $grandparent); + $uncle = new RedBlackNode(30, $grandparent); + + $grandparent->setLeft($parent); + $grandparent->setRight($uncle); + + $child = new RedBlackNode(5, $parent); + $parent->setLeft($child); + + $this->assertSame($uncle, $child->uncle()); + } + + public function testSibling(): void + { + $parent = new RedBlackNode(20); + $left = new RedBlackNode(10, $parent); + $right = new RedBlackNode(30, $parent); + $parent->setLeft($left); + $parent->setRight($right); + + $this->assertEquals($right, $left->sibling()); + $this->assertEquals($left, $right->sibling()); + } + + public function testRotateWithRight(): void + { + $grandparent = new RedBlackNode(10); + $redBlackTree = new RedBlackTree($grandparent); + $node = new RedBlackNode(5, $grandparent); + $grandparent->setLeft($node); + + $nodeChildLeft = new RedBlackNode(3, $node); + $node->setLeft($nodeChildLeft); + $nodeChildRight = new RedBlackNode(7, $node); + $node->setRight($nodeChildRight); + + $leftGrandChild = new RedBlackNode(1, $nodeChildLeft); + $nodeChildLeft->setLeft($leftGrandChild); + $rightGrandChild = new RedBlackNode(4, $nodeChildLeft); + $nodeChildLeft->setRight($rightGrandChild); + + $node->rotate($redBlackTree, Direction::Right); + + $this->assertSame($grandparent->getLeft(), $nodeChildLeft); + $this->assertSame($nodeChildLeft->getParent(), $grandparent); + + $this->assertSame($node->getParent(), $nodeChildLeft); + $this->assertSame($nodeChildLeft->getRight(), $node); + + $this->assertSame($node->getLeft(), $rightGrandChild); + $this->assertSame($rightGrandChild->getParent(), $node); + + $this->assertSame($node->getRight(), $nodeChildRight); + $this->assertSame($nodeChildRight->getParent(), $node); + } + + public function testRotateWithLeft(): void + { + $grandparent = new RedBlackNode(10); + $redBlackTree = new RedBlackTree($grandparent); + $node = new RedBlackNode(5, $grandparent); + $grandparent->setLeft($node); + + $nodeChildLeft = new RedBlackNode(3, $node); + $node->setLeft($nodeChildLeft); + $nodeChildRight = new RedBlackNode(7, $node); + $node->setRight($nodeChildRight); + + $leftGrandChild = new RedBlackNode(6, $nodeChildRight); + $nodeChildRight->setLeft($leftGrandChild); + $rightGrandChild = new RedBlackNode(8, $nodeChildRight); + $nodeChildRight->setRight($rightGrandChild); + + $node->rotate($redBlackTree, Direction::Left); + + $this->assertSame($grandparent->getLeft(), $nodeChildRight); + $this->assertSame($nodeChildRight->getParent(), $grandparent); + + $this->assertSame($nodeChildRight->getLeft(), $node); + $this->assertSame($node->getParent(), $nodeChildRight); + + $this->assertSame($node->getRight(), $leftGrandChild); + $this->assertSame($leftGrandChild->getParent(), $node); + + $this->assertSame($nodeChildRight->getRight(), $rightGrandChild); + $this->assertSame($rightGrandChild->getParent(), $nodeChildRight); + } + + public function testDeleteRootNode(): void + { + $tree = new RedBlackTree(); + + $tree->insert(10); + + $tree->delete($tree->getRoot()); + + $this->assertNull($tree->getRoot()); + } + + public function testDeleteLeftLeafNode(): void + { + $tree = new RedBlackTree(); + + $tree->insert(10); + $tree->insert(5); + $tree->insert(15); + + $leafNode = $tree->getRoot()->getLeft(); + $tree->delete($leafNode); + + $this->assertNull($tree->getRoot()->getLeft()); + } + + public function testDeleteNodeWithOneChild(): void + { + $tree = new RedBlackTree(); + + $tree->insert(10); + $tree->insert(15); + $tree->insert(5); + $tree->insert(2); + + $nodeWithOneChild = $tree->getRoot()->getLeft(); + $tree->delete($nodeWithOneChild); + + $this->assertEquals(2, $tree->getRoot()->getLeft()->getValue()); + } + + public function testDeleteNodeWithTwoChildren(): void + { + $tree = new RedBlackTree(); + $tree->insert(4); + $tree->insert(2); + $tree->insert(6); + $tree->insert(1); + $tree->insert(3); + $tree->insert(5); + $tree->insert(7); + + $nodeWithTwoChildren = $tree->getRoot()->getLeft(); + $tree->delete($nodeWithTwoChildren); + $this->assertEquals(1, $tree->getRoot()->getLeft()->getValue()); + $this->assertNull($tree->getRoot()->getLeft()->getLeft()); + } + + public function testDeleteRightLeafNode(): void + { + $tree = new RedBlackTree(); + + $tree->insert(10); + $tree->insert(5); + $tree->insert(15); + + $leafNode = $tree->getRoot()->getRight(); + $tree->delete($leafNode); + + $this->assertNull($tree->getRoot()->getRight()); + } + + private function isBalancedRedBlackTree(?RedBlackNode $node): bool + { + if ($node === null) { + return true; + } + + $left = $node->getLeft(); + $right = $node->getRight(); + + if ($node->getColor() === Color::Red) { + if ($left) { + $this->assertEquals(Color::Black, $left->getColor()); + } + if ($right) { + $this->assertEquals(Color::Black, $right?->getColor()); + } + } + + return $this->isBalancedRedBlackTree($left) && $this->isBalancedRedBlackTree($right); + } +}