From 620f7af78034564753d4a63609ac384d70db5d3e Mon Sep 17 00:00:00 2001 From: Walmir Silva Date: Tue, 2 Jul 2024 15:18:35 -0300 Subject: [PATCH 1/4] add classes base for BPlusTree algorithm --- README.md | 440 +++++++++++++----- composer.lock | 10 +- src/Tree/BPlusTree.php | 293 ++++++++++++ .../BPlusTreeNode/BPlusTreeInternalNode.php | 83 ++++ src/Tree/BPlusTreeNode/BPlusTreeLeafNode.php | 80 ++++ src/Tree/BPlusTreeNode/BPlusTreeNode.php | 23 + tests/Tree/BPlusTreeTest.php | 199 ++++++++ 7 files changed, 1016 insertions(+), 112 deletions(-) create mode 100644 src/Tree/BPlusTree.php create mode 100644 src/Tree/BPlusTreeNode/BPlusTreeInternalNode.php create mode 100644 src/Tree/BPlusTreeNode/BPlusTreeLeafNode.php create mode 100644 src/Tree/BPlusTreeNode/BPlusTreeNode.php create mode 100644 tests/Tree/BPlusTreeTest.php diff --git a/README.md b/README.md index 820c266..738f3ce 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# KaririCode Contract +# KaririCode Framework: Data Structures Component [![en](https://img.shields.io/badge/lang-en-red.svg)](README.md) [![pt-br](https://img.shields.io/badge/lang-pt--br-green.svg)](README.pt-br.md) @@ -10,162 +10,388 @@ ## Overview -The `kariricode/kariricode-data-structure` package provides a set of standardized interfaces for common data structures and patterns within the KaririCode Framework. This library ensures consistency and interoperability across various components of the KaririCode ecosystem, following PSR standards and utilizing modern PHP practices. +The Data Structures component is a cornerstone of the KaririCode Framework, offering robust, high-performance, and type-safe implementations of essential data structures for PHP applications. This component is meticulously designed to meet the demands of modern, scalable software development, providing developers with a powerful toolkit to optimize their applications' data management capabilities. -## Features +## Key Features -- **🗂️ PSR Standards**: Adheres to PHP-FIG PSR standards for interoperability. -- **📚 Comprehensive Interfaces**: Includes interfaces for common data structures such as Collection, Heap, Map, Queue, Stack, and Tree. -- **🚀 Modern PHP**: Utilizes PHP 8.3 features to ensure type safety and modern coding practices. -- **🔍 High Quality**: Ensures code quality and security through rigorous testing and analysis tools. +- **Optimized Performance**: Carefully crafted implementations ensure optimal time and space complexity for all operations. +- **Type Safety**: Leverages PHP 8.0+ features for enhanced type checking and improved code reliability. +- **Memory Efficiency**: Implements custom memory management strategies to minimize overhead. +- **Iterative and Recursive API**: Offers both iterative and recursive methods for key operations, allowing developers to choose based on their specific use case. +- **Serialization Support**: All data structures implement PHP's Serializable interface for easy storage and transmission. +- **Extensive Testing**: Comprehensive unit and integration tests ensure reliability and correctness. +- **PSR Compliance**: Adheres to PHP-FIG standards for coding style (PSR-12) and autoloading (PSR-4). -## Installation +## Available Data Structures -You can install the package via Composer: +### TreeSet -```bash -composer require kariricode/kariricode-data-structure +An ordered set implementation based on a self-balancing binary search tree (Red-Black Tree). + +#### Complexity Analysis + +- Time Complexity: + - Add, Remove, Contains: O(log n) + - Minimum/Maximum: O(log n) + - Iteration: O(n) +- Space Complexity: O(n) + +#### Key Methods + +```php +public function add($element): void +public function remove($element): bool +public function contains($element): bool +public function union(TreeSet $other): TreeSet +public function intersection(TreeSet $other): TreeSet +public function difference(TreeSet $other): TreeSet +public function find(mixed $element): ?mixed +``` + +#### Usage Example + +```php +use KaririCode\DataStructure\Set\TreeSet; + +$set = new TreeSet(); +$set->add(5); +$set->add(3); +$set->add(7); +echo $set->contains(3); // Output: true +echo $set->find(5); // Output: 5 ``` -## Usage +### ArrayDeque + +A double-ended queue using a dynamic circular array. -Implement the provided interfaces in your classes to ensure consistent and reliable functionality across different components of the KaririCode Framework. +#### Complexity Analysis -Example of implementing the `CollectionList` interface: +- Time Complexity: + - AddFirst, AddLast, RemoveFirst, RemoveLast: Amortized O(1) + - Get, Set: O(1) +- Space Complexity: O(n) + +#### Key Methods ```php -addFirst(1); +$deque->addLast(2); +echo $deque->removeFirst(); // Output: 1 +echo $deque->removeLast(); // Output: 2 +``` -class MyCollection implements CollectionList -{ - private array $items = []; - - public function add(mixed $item): void - { - $this->items[] = $item; - } - - public function remove(mixed $item): bool - { - $index = array_search($item, $this->items, true); - if ($index === false) { - return false; - } - unset($this->items[$index]); - return true; - } - - public function get(int $index): mixed - { - return $this->items[$index] ?? null; - } - - public function clear(): void - { - $this->items = []; - } - - public function getIterator(): \Traversable - { - return new \ArrayIterator($this->items); - } - - public function count(): int - { - return count($this->items); - } - - public function offsetExists(mixed $offset): bool - { - return isset($this->items[$offset]); - } - - public function offsetGet(mixed $offset): mixed - { - return $this->items[$offset] ?? null; - } - - public function offsetSet(mixed $offset, mixed $value): void - { - if ($offset === null) { - $this->items[] = $value; - } else { - $this->items[$offset] = $value; - } - } - - public function offsetUnset(mixed $offset): void - { - unset($this->items[$offset]); - } -} +### ArrayQueue + +A simple queue using a circular array, providing amortized O(1) time complexity for enqueue and dequeue operations. + +#### Complexity Analysis + +- Time Complexity: + - Enqueue, Dequeue: Amortized O(1) +- Space Complexity: O(n) + +#### Key Methods + +```php +public function enqueue(mixed $element): void +public function dequeue(): mixed +public function peek(): mixed +public function isEmpty(): bool +public function size(): int +public function clear(): void +public function add(mixed $element): void +public function removeFirst(): mixed +``` + +#### Usage Example + +```php +use KaririCode\DataStructure\Queue\ArrayQueue; + +$queue = new ArrayQueue(); +$queue->add(1); +$queue->enqueue(2); +echo $queue->dequeue(); // Output: 1 +``` + +### TreeMap + +A map implementation based on a self-balancing binary search tree (Red-Black Tree). + +#### Complexity Analysis + +- Time Complexity: + - Put, Get, Remove: O(log n) + - ContainsKey: O(log n) + - Iteration: O(n) +- Space Complexity: O(n) + +#### Key Methods + +```php +public function put($key, $value): void +public function get($key): ?mixed +public function remove($key): bool +public function containsKey($key): bool +public function keys(): array +public function values(): array +public function clear(): void +public function getItems(): array +``` + +#### Usage Example + +```php +use KaririCode\DataStructure\Map\TreeMap; + +$map = new TreeMap(); +$map->put("one", 1); +$map->put("two", 2); +echo $map->get("one"); // Output: 1 +$map->remove("two"); +echo $map->containsKey("two"); // Output: false +``` + +### LinkedList + +A doubly-linked list implementation providing efficient insertions and deletions. + +#### Complexity Analysis + +- Time Complexity: + - Add, Remove: O(1) + - Get, Set: O(n) + - Iteration: O(n) +- Space Complexity: O(n) + +#### Key Methods + +```php +public function add($element): void +public function remove($element): bool +public function contains($element): bool +public function get(int $index): mixed +public function set(int $index, $element): void +public function clear(): void +public function size(): int +public function getItems(): array +``` + +#### Usage Example + +```php +use KaririCode\DataStructure\Collection\LinkedList; + +$list = new LinkedList(); +$list->add("first"); +$list->add("second"); +echo $list->get(1); // Output: "second" +$list->remove("first"); +echo $list->contains("first"); // Output: false +``` + +### BinaryHeap + +A binary heap implementation supporting both min-heap and max-heap functionality. + +#### Complexity Analysis + +- Time Complexity: + - Insert, ExtractMin/Max: O(log n) + - PeekMin/Max: O(1) +- Space Complexity: O(n) + +#### Key Methods + +```php +public function insert($element): void +public function extractMin(): mixed // For MinHeap +public function extractMax(): mixed // For MaxHeap +public function peek(): mixed +public function size(): int +public function isEmpty(): bool +``` + +#### Usage Example + +```php +use KaririCode\DataStructure\BinaryHeap; + +$heap = new BinaryHeap(); +$heap->insert(5); +$heap->insert(3); +$heap->insert(7); +echo $heap->extractMin(); // Output: 3 +echo $heap->peek(); // Output: 5 +``` + +### HashMap + +A hash map using PHP's built-in array as the underlying storage, providing O(1) average time complexity for put, get, and remove operations. + +#### Complexity Analysis + +- Time Complexity: + - Put, Get, Remove: Average O(1), Worst O(n) + - ContainsKey: Average O(1), Worst O(n) +- Space Complexity: O(n) + +#### Key Methods + +```php +public function put($key, $value): void +public function get($key): ?mixed +public function remove($key): bool +public function containsKey($key): bool +public function keys(): array +public function values(): array +public function clear(): void +public function size(): int +public function getIterator(): \Iterator +``` + +#### Usage Example + +```php +use KaririCode\DataStructure\Map\HashMap; + +$map = new HashMap(); +$map->put("one", 1); +$map->put("two", 2); +echo $map->get("one"); // Output: 1 +$map->remove("two"); +echo $map->containsKey("two"); // Output: false +``` + +### BinaryHeap + +A binary heap implementation supporting both min-heap and max-heap functionality. + +#### Complexity Analysis + +- Time Complexity: + - Insert, ExtractMin/Max: O(log n) + - PeekMin/Max: O(1) +- Space Complexity: O(n) + +#### Key Methods + +```php +public function insert($element): void +public function extractMin(): mixed // For MinHeap +public function extractMax(): mixed // For MaxHeap +public function peek(): mixed +public function size(): int +public function isEmpty(): bool ``` -## Development Environment +#### Usage Example + +```php +use KaririCode\DataStructure\BinaryHeap; + +$heap = new BinaryHeap('min'); +$heap->add(5); +$heap->add(3); +$heap->add(7); +echo $heap->poll(); // Output: 3 +echo $heap->peek(); // Output: 5 +``` -### Docker +## Installation -To maintain consistency and ensure the environment's integrity, we provide a Docker setup: +### Requirements -- **🐳 Docker Compose**: Used to manage multi-container Docker applications. -- **📦 Dockerfile**: Defines the Docker image for the PHP environment. +- PHP 8.0 or higher +- Composer -To start the environment: +### Via Composer ```bash -make up +composer require kariricode/data-structures ``` -### Makefile +### Manual Installation -We include a `Makefile` to streamline common development tasks: +Add the following to your `composer.json`: -- **Start services**: `make up` -- **Stop services**: `make down` -- **Run tests**: `make test` -- **Install dependencies**: `make composer-install` -- **Run code style checks**: `make cs-check` -- **Fix code style issues**: `make cs-fix` -- **Security checks**: `make security-check` +```json +{ + "require": { + "kariricode/data-structures": "^1.0" + } +} +``` -For a complete list of commands, run: +Then run: ```bash -make help +composer update ``` ## Testing -To run the tests, you can use the following command: +To run tests, use PHPUnit. Ensure you have PHPUnit installed and configured. You can run the tests using the following command: ```bash -make test +vendor/bin/phpunit --testdox ``` ## Contributing -Contributions are welcome! Please read our [contributing guidelines](CONTRIBUTING.md) for details on the process for submitting pull requests. +We welcome contributions from the community! Please read our [Contributing Guide](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests. -## Support +### Development Setup -For any issues, please visit our [issue tracker](https://github.com/Kariri-PHP-Framework/kariri-contract/issues). +1. Fork and clone the repository. +2. Install dependencies: `composer install`. +3. Run tests: `./vendor/bin/phpunit`. +4. Submit a pull request. ## License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. -## About KaririCode +## Support and Community + +- **Documentation**: [https://docs.kariricode.com/data-structures](https://docs.kariricode.com/data-structures) +- **Issue Tracker**: [GitHub Issues](https://github.com/kariricode/data-structures/issues) +- **Community Forum**: [KaririCode Community](https://community.kariricode.com) +- **Professional Support**: For enterprise-grade support, contact us at enterprise@kariricode.com -The KaririCode Framework is a modern, robust, and scalable PHP framework designed to streamline web development by providing a comprehensive set of tools and components. For more information, visit the [KaririCode website](https://kariricode.org/). +## Acknowledgments -Join the KaririCode Club for access to exclusive content, community support, and advanced tutorials on PHP and the KaririCode Framework. Learn more at [KaririCode Club](https://kariricode.org/club). +- The KaririCode Framework team and contributors. +- The PHP community for their continuous support and inspiration. +- [PHPBench](https://github.com/phpbench/phpbench) for performance benchmarking tools. + +## Roadmap + +- [ ] Implement Skip List data structure. +- [ ] Add support for concurrent access in HashMap. +- [ ] Develop a B-Tree implementation for large datasets. +- [ ] Enhance documentation with more real-world use cases. +- [ ] Implement a graph data structure and common algorithms. --- +Built with ❤️ by the KaririCode team. Empowering developers to build faster, more efficient PHP applications. + Maintained by Walmir Silva - [walmir.silva@kariricode.org](mailto:walmir.silva@kariricode.org) diff --git a/composer.lock b/composer.lock index 5b825b9..4fc0d38 100644 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "kariricode/contract", - "version": "v2.3.1", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/KaririCode-Framework/kariricode-contract.git", - "reference": "619c2f472a874a59b118460d7ac55e4eaa07b636" + "reference": "8aba8e4abddaf5006bd2c88af4be8de4779e32a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-contract/zipball/619c2f472a874a59b118460d7ac55e4eaa07b636", - "reference": "619c2f472a874a59b118460d7ac55e4eaa07b636", + "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-contract/zipball/8aba8e4abddaf5006bd2c88af4be8de4779e32a4", + "reference": "8aba8e4abddaf5006bd2c88af4be8de4779e32a4", "shasum": "" }, "require": { @@ -66,7 +66,7 @@ "issues": "https://github.com/KaririCode-Framework/kariricode-contract/issues", "source": "https://github.com/KaririCode-Framework/kariricode-contract" }, - "time": "2024-07-01T21:51:33+00:00" + "time": "2024-07-02T13:40:09+00:00" } ], "packages-dev": [ diff --git a/src/Tree/BPlusTree.php b/src/Tree/BPlusTree.php new file mode 100644 index 0000000..710e9ba --- /dev/null +++ b/src/Tree/BPlusTree.php @@ -0,0 +1,293 @@ +order = $order; + $this->root = null; + } + + public function add(mixed $element): void + { + $this->insert($element, $element); + } + + public function insert(int $index, mixed $value): void + { + if ($this->root === null) { + $this->root = new BPlusTreeLeafNode($this->order); + } + + $this->root = $this->root->insert($index, $value); + + if ($this->root->isFull()) { + $newRoot = new BPlusTreeInternalNode($this->order); + $newRoot->children[] = $this->root; + $this->root = $newRoot->split(); + } + + $this->size++; + } + + public function remove(mixed $element): bool + { + if ($this->root === null) { + return false; + } + + $result = $this->root->remove($element); + if ($result) { + $this->size--; + if ($this->root instanceof BPlusTreeInternalNode && count($this->root->keys) === 0) { + $this->root = $this->root->children[0]; + } + } + + return $result; + } + + public function clear(): void + { + $this->root = null; + $this->size = 0; + } + + public function contains(mixed $element): bool + { + return $this->find($element) !== null; + } + + public function find(mixed $element): mixed + { + if ($this->root === null) { + return null; + } + if (is_int($element)) { + return $this->root->search($element); + } else { + return $this->searchByValue($element); + } + } + + private function searchByValue(mixed $value): ?int + { + $current = $this->getLeftmostLeaf(); + while ($current !== null) { + foreach ($current->values as $index => $nodeValue) { + if ($nodeValue === $value) { + return $current->keys[$index]; + } + } + $current = $current->next; + } + return null; + } + + public function get(int $index): mixed + { + if ($index < 0 || $index >= $this->size) { + throw new \OutOfRangeException("Index out of range"); + } + + $current = $this->root; + while ($current instanceof BPlusTreeInternalNode) { + $current = $current->children[0]; + } + + /** @var BPlusTreeLeafNode $current */ + for ($i = 0; $i < $index; $i++) { + $current = $current->next; + if ($current === null) { + throw new \OutOfRangeException("Index out of range"); + } + } + + return $current->values[0]; + } + + public function set(int $index, mixed $element): void + { + if ($index < 0 || $index >= $this->size) { + throw new \OutOfRangeException("Index out of range"); + } + + $current = $this->getLeftmostLeaf(); + $currentIndex = 0; + + while ($current !== null) { + for ($i = 0; $i < count($current->keys); $i++) { + if ($current->keys[$i] == $index) { + $current->values[$i] = $element; + return; + } + $currentIndex++; + } + $current = $current->next; + } + + throw new \OutOfRangeException("Index not found"); + } + + public function size(): int + { + return $this->size; + } + + public function getItems(): array + { + $items = []; + $current = $this->getLeftmostLeaf(); + while ($current !== null) { + $items = array_merge($items, $current->values); + $current = $current->next; + } + return $items; + } + + public function addAll(Collection $collection): void + { + foreach ($collection->getItems() as $item) { + $this->add($item); + } + } + + public function getOrder(): int + { + return $this->order; + } + + // In BPlusTree.php + + public function rangeSearch(mixed $start, mixed $end): array + { + $result = []; + $current = $this->root; + + // Find the leaf node where the range starts + while ($current instanceof BPlusTreeInternalNode) { + $i = 0; + while ($i < count($current->keys) && $start > $current->keys[$i]) { + $i++; + } + $current = $current->children[$i]; + } + + // Collect all values in the range + /** @var BPlusTreeLeafNode $current */ + while ($current !== null) { + foreach ($current->values as $key => $value) { + if ($current->keys[$key] >= $start && $current->keys[$key] <= $end) { + $result[] = $value; + } + if ($current->keys[$key] > $end) { + return $result; + } + } + $current = $current->next; + } + + return $result; + } + + public function getMinimum(): mixed + { + $leftmostLeaf = $this->getLeftmostLeaf(); + return $leftmostLeaf !== null ? $leftmostLeaf->values[0] : null; + } + + public function getMaximum(): mixed + { + $rightmostLeaf = $this->getRightmostLeaf(); + return $rightmostLeaf !== null ? $rightmostLeaf->values[count($rightmostLeaf->values) - 1] : null; + } + + public function balance(): void + { + // A B+ Tree is self-balancing, so we don't need to implement any additional balancing logic. + // However, we can perform a check to ensure the tree is balanced. + $this->checkBalance($this->root); + } + + private function checkBalance(?BPlusTreeNode $node): int + { + if ($node === null) { + return 0; + } + + if ($node instanceof BPlusTreeLeafNode) { + return 1; + } + + + /** @var BPlusTreeInternalNode $node */ + $height = $this->checkBalance($node->children[0]); + + for ($i = 1; $i < count($node->children); $i++) { + $childHeight = $this->checkBalance($node->children[$i]); + if ($childHeight !== $height) { + throw new \RuntimeException("B+ Tree is not balanced"); + } + } + + return $height + 1; + } + + public function sort(): void + { + // B+ Tree is always sorted, so this method doesn't need to do anything. + // However, we can perform a check to ensure the tree is sorted. + $this->checkSorted(); + } + + private function checkSorted(): void + { + $current = $this->getLeftmostLeaf(); + $prev = null; + + while ($current !== null) { + foreach ($current->values as $value) { + if ($prev !== null && $value < $prev) { + throw new \RuntimeException("B+ Tree is not sorted"); + } + $prev = $value; + } + $current = $current->next; + } + } + + private function getLeftmostLeaf(): ?BPlusTreeLeafNode + { + $current = $this->root; + while ($current instanceof BPlusTreeInternalNode) { + $current = $current->children[0]; + } + return $current; + } + + private function getRightmostLeaf(): ?BPlusTreeLeafNode + { + $current = $this->root; + while ($current instanceof BPlusTreeInternalNode) { + $current = $current->children[count($current->children) - 1]; + } + return $current; + } +} diff --git a/src/Tree/BPlusTreeNode/BPlusTreeInternalNode.php b/src/Tree/BPlusTreeNode/BPlusTreeInternalNode.php new file mode 100644 index 0000000..f5a4abf --- /dev/null +++ b/src/Tree/BPlusTreeNode/BPlusTreeInternalNode.php @@ -0,0 +1,83 @@ +findInsertionIndex($key); + $this->children[$insertionIndex] = $this->children[$insertionIndex]->insert($key, $value); + + if ($this->children[$insertionIndex] instanceof BPlusTreeInternalNode) { + $this->keys = array_merge( + array_slice($this->keys, 0, $insertionIndex), + $this->children[$insertionIndex]->keys, + array_slice($this->keys, $insertionIndex) + ); + $this->children = array_merge( + array_slice($this->children, 0, $insertionIndex), + $this->children[$insertionIndex]->children, + array_slice($this->children, $insertionIndex + 1) + ); + } + + if ($this->isFull()) { + return $this->split(); + } + + return $this; + } + + public function remove(mixed $key): bool + { + $index = $this->findInsertionIndex($key); + return $this->children[$index]->remove($key); + } + + + public function search(mixed $key): mixed + { + $index = $this->findInsertionIndex($key); + return $this->children[$index]->search($key); + } + + private function findInsertionIndex(mixed $key): int + { + $left = 0; + $right = count($this->keys); + + while ($left < $right) { + $mid = ($left + $right) >> 1; + if ($this->keys[$mid] <= $key) { + $left = $mid + 1; + } else { + $right = $mid; + } + } + + return $left; + } + + public function split(): BPlusTreeInternalNode + { + $middle = (int)($this->order / 2); + + $newNode = new BPlusTreeInternalNode($this->order); + $newNode->keys = array_splice($this->keys, $middle + 1); + $newNode->children = array_splice($this->children, $middle + 1); + + $parent = new BPlusTreeInternalNode($this->order); + $parent->keys[] = $this->keys[$middle]; + $parent->children = [$this, $newNode]; + + array_pop($this->keys); + + return $parent; + } +} diff --git a/src/Tree/BPlusTreeNode/BPlusTreeLeafNode.php b/src/Tree/BPlusTreeNode/BPlusTreeLeafNode.php new file mode 100644 index 0000000..76a5cc9 --- /dev/null +++ b/src/Tree/BPlusTreeNode/BPlusTreeLeafNode.php @@ -0,0 +1,80 @@ +findInsertionIndex($key); + + array_splice($this->keys, $insertionIndex, 0, [$key]); + array_splice($this->values, $insertionIndex, 0, [$value]); + + if ($this->isFull()) { + return $this->split(); + } + + return $this; + } + + public function remove(mixed $key): bool + { + $index = $this->findInsertionIndex($key); + if ($index < count($this->keys) && $this->keys[$index] === $key) { + array_splice($this->keys, $index, 1); + array_splice($this->values, $index, 1); + return true; + } + return false; + } + + + public function search(mixed $key): mixed + { + $index = $this->findInsertionIndex($key); + if ($index < count($this->keys) && $this->keys[$index] === $key) { + return $this->values[$index]; + } + return null; + } + + private function findInsertionIndex(mixed $key): int + { + $left = 0; + $right = count($this->keys); + + while ($left < $right) { + $mid = ($left + $right) >> 1; + if ($this->keys[$mid] < $key) { + $left = $mid + 1; + } else { + $right = $mid; + } + } + + return $left; + } + + private function split(): BPlusTreeInternalNode + { + $middle = (int)($this->order / 2); + + $newNode = new BPlusTreeLeafNode($this->order); + $newNode->keys = array_splice($this->keys, $middle); + $newNode->values = array_splice($this->values, $middle); + $newNode->next = $this->next; + $this->next = $newNode; + + $parent = new BPlusTreeInternalNode($this->order); + $parent->keys[] = $newNode->keys[0]; + $parent->children = [$this, $newNode]; + + return $parent; + } +} diff --git a/src/Tree/BPlusTreeNode/BPlusTreeNode.php b/src/Tree/BPlusTreeNode/BPlusTreeNode.php new file mode 100644 index 0000000..81be6dd --- /dev/null +++ b/src/Tree/BPlusTreeNode/BPlusTreeNode.php @@ -0,0 +1,23 @@ +keys) >= $this->order - 1; + } + + abstract public function insert(int $key, mixed $value): BPlusTreeNode; + abstract public function remove(mixed $key): bool; + abstract public function search(mixed $input): mixed; +} diff --git a/tests/Tree/BPlusTreeTest.php b/tests/Tree/BPlusTreeTest.php new file mode 100644 index 0000000..43ba996 --- /dev/null +++ b/tests/Tree/BPlusTreeTest.php @@ -0,0 +1,199 @@ +tree = new BPlusTree(3); // B+ Tree of order 3 + } + + // public function testGetThrowsOutOfRangeException(): void + // { + // $this->expectException(\OutOfRangeException::class); + // $this->tree->get(0); + // } + + // Test insertion and root splitting + public function testInsertAndSearch(): void + { + $this->tree->insert(1, 'a'); + $this->tree->insert(2, 'b'); + $this->tree->insert(3, 'c'); + + // Test search by key + $this->assertSame('a', $this->tree->find(1)); + $this->assertSame('b', $this->tree->find(2)); + $this->assertSame('c', $this->tree->find(3)); + + // Test search by value + $this->assertSame(1, $this->tree->find('a')); + $this->assertSame(2, $this->tree->find('b')); + $this->assertSame(3, $this->tree->find('c')); + } + + + + // Test removal and balancing + public function testRemoveAndBalance(): void + { + $this->tree->insert(1, 'a'); + $this->tree->insert(2, 'b'); + $this->tree->insert(3, 'c'); + $this->tree->remove(2); + + $this->assertFalse($this->tree->contains(2)); + $this->assertTrue($this->tree->contains(1)); + $this->assertTrue($this->tree->contains(3)); + } + + // Test clear method + public function testClear(): void + { + $this->tree->insert(1, 'a'); + $this->tree->insert(2, 'b'); + $this->tree->clear(); + + $this->assertSame(0, $this->tree->size()); + $this->assertNull($this->tree->find(1)); + $this->assertNull($this->tree->find(2)); + } + + // Test contains method + public function testContains(): void + { + $this->tree->insert(1, 'a'); + $this->tree->insert(2, 'b'); + + $this->assertTrue($this->tree->contains(1)); + $this->assertTrue($this->tree->contains(2)); + $this->assertFalse($this->tree->contains(3)); + } + + // Test find method + public function testFind(): void + { + $this->tree->insert(1, 'a'); + $this->assertSame('a', $this->tree->find(1)); + $this->assertNull($this->tree->find(2)); + } + + // Test get method + public function testGet(): void + { + $this->tree->insert(1, 'a'); + $this->tree->insert(2, 'b'); + $this->tree->insert(3, 'c'); + + $this->assertSame('a', $this->tree->get(0)); + $this->assertSame('b', $this->tree->get(1)); + $this->assertSame('c', $this->tree->get(2)); + } + + // Test set method + public function testSet(): void + { + $this->tree->insert(1, 'a'); + $this->tree->insert(2, 'b'); + $this->tree->set(1, 'new-a'); + + $this->assertSame('new-a', $this->tree->find(1)); + $this->assertSame('b', $this->tree->find(2)); + } + + // Test size method + public function testSize(): void + { + $this->assertSame(0, $this->tree->size()); + $this->tree->insert(1, 'a'); + $this->assertSame(1, $this->tree->size()); + } + + // Test getItems method + public function testGetItems(): void + { + $this->tree->insert(1, 'a'); + $this->tree->insert(2, 'b'); + $this->tree->insert(3, 'c'); + + $this->assertSame(['a', 'b', 'c'], $this->tree->getItems()); + } + + // Test addAll method + public function testAddAll(): void + { + $collection = $this->createMock(Collection::class); + $collection->method('getItems')->willReturn([1, 2, 3]); + + $this->tree->addAll($collection); + + $this->assertSame(3, $this->tree->size()); + } + + // Test getOrder method + public function testGetOrder(): void + { + $this->assertSame(3, $this->tree->getOrder()); + } + + // Test rangeSearch method + public function testRangeSearch(): void + { + $this->tree->insert(1, 'a'); + $this->tree->insert(2, 'b'); + $this->tree->insert(3, 'c'); + + $this->assertSame(['a', 'b'], $this->tree->rangeSearch(1, 2)); + } + + // Test getMinimum method + public function testGetMinimum(): void + { + $this->tree->insert(2, 'b'); + $this->tree->insert(1, 'a'); + $this->tree->insert(3, 'c'); + + $this->assertSame('a', $this->tree->getMinimum()); + } + + // Test getMaximum method + public function testGetMaximum(): void + { + $this->tree->insert(1, 'a'); + $this->tree->insert(3, 'c'); + $this->tree->insert(2, 'b'); + + $this->assertSame('c', $this->tree->getMaximum()); + } + + // Test balance method + public function testBalance(): void + { + $this->tree->insert(1, 'a'); + $this->tree->insert(2, 'b'); + $this->tree->balance(); + + $this->assertSame(['a', 'b'], $this->tree->getItems()); + } + + // Test sort method + public function testSort(): void + { + $this->tree->insert(3, 'c'); + $this->tree->insert(1, 'a'); + $this->tree->insert(2, 'b'); + $this->tree->sort(); + + $this->assertSame(['a', 'b', 'c'], $this->tree->getItems()); + } +} From a547e33748420dc966103d95cccc89ba0e42eb67 Mon Sep 17 00:00:00 2001 From: Walmir Silva Date: Mon, 23 Sep 2024 18:30:52 -0300 Subject: [PATCH 2/4] Fix B+ Tree implementation and tests to address node splitting, key promotion, and key-based access - **Adjusted `BPlusTreeInternalNode::insert()` Method:** - Corrected the insertion logic to maintain proper alignment between `keys` and `children` arrays. - Ensured that when a child node splits, the promoted key is inserted into the current node's `keys`, and the new child node is correctly placed in the `children` array. - **Modified `split()` Methods in Nodes:** - Updated `BPlusTreeInternalNode::split()` to correctly calculate the middle index and properly split `keys` and `children` arrays. - Promoted the appropriate key to the parent node during splits. - Ensured that each internal node maintains the B+ Tree properties after splitting. - Adjusted `BPlusTreeLeafNode::split()` to handle small orders and maintain the linked list of leaf nodes. - **Updated Tree Size Management:** - Incremented the `size` property in the `BPlusTree` class during insertions to accurately reflect the number of elements. - Fixed issues where the `size` was not updated, causing `OutOfRangeException` errors. - **Revised `set` and `get` Methods:** - Changed `set` and `get` methods to operate on keys rather than indices, aligning with the key-based nature of B+ Trees. - Updated `set` to traverse the tree and update the value associated with a given key. - Modified `get` to retrieve the value based on the key, throwing an exception if the key is not found. - **Fixed Test Cases:** - Adjusted `testVisualTreeStructure` to insert enough keys to cause node splitting, ensuring both internal and leaf nodes are present. - Updated `testGetOrder` by correcting the `order` property assignment in the `BPlusTree` class, matching expected values in tests. - Renamed and modified `testSetAndGetByIndex` to `testSetAndGetByKey`, reflecting the changes in method functionality. - **Improved Code Consistency and Documentation:** - Removed redundant property declarations and assignments in the `BPlusTree` constructor. - Ensured consistent use of the `order` property throughout the class. - Added comments and documentation to clarify method purposes and enhance readability. - Ensured alignment between method implementations and their intended use cases. **Summary:** These corrections address critical issues in the B+ Tree implementation related to node splitting, key promotion, and data access by keys. By refining the logic in node insertion and splitting methods, the tree now correctly maintains its balanced structure and properties, even with small orders. The `size` property accurately tracks the number of elements, preventing out-of-range errors. The `set` and `get` methods now intuitively operate on keys, improving usability. Tests have been updated to reflect these changes, ensuring the reliability and correctness of the implementation. --- .gitignore | 2 + src/Tree/BPlusTree.php | 247 ++++---- .../BPlusTreeNode/BPlusTreeInternalNode.php | 95 +-- src/Tree/BPlusTreeNode/BPlusTreeLeafNode.php | 67 ++- src/Tree/BPlusTreeNode/BPlusTreeNode.php | 17 +- src/Tree/BPlusTreeSearcher.php | 135 +++++ .../BPlusTreeInternalNodeTest.php | 74 +++ .../BPlusTreeNode/BPlusTreeLeafNodeTest.php | 92 +++ tests/Tree/BPlusTreeTest.php | 217 +++---- tests/bplus_tree_example.php | 383 ++++++++++++ tests/bplus_tree_example2.php | 380 ++++++++++++ tests/bplus_tree_example3.php | 404 +++++++++++++ tests/bplus_tree_example4.php | 493 ++++++++++++++++ tests/bplus_tree_example5.php | 543 ++++++++++++++++++ 14 files changed, 2831 insertions(+), 318 deletions(-) create mode 100644 src/Tree/BPlusTreeSearcher.php create mode 100644 tests/Tree/BPlusTreeNode/BPlusTreeInternalNodeTest.php create mode 100644 tests/Tree/BPlusTreeNode/BPlusTreeLeafNodeTest.php create mode 100644 tests/bplus_tree_example.php create mode 100644 tests/bplus_tree_example2.php create mode 100644 tests/bplus_tree_example3.php create mode 100644 tests/bplus_tree_example4.php create mode 100644 tests/bplus_tree_example5.php diff --git a/.gitignore b/.gitignore index 36acca2..bb3d948 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,5 @@ temp/ tmp/ .vscode/launch.json .vscode/extensions.json +lista_de_arquivos.txt +tests/lista_de_arquivos.php diff --git a/src/Tree/BPlusTree.php b/src/Tree/BPlusTree.php index 710e9ba..e979ed2 100644 --- a/src/Tree/BPlusTree.php +++ b/src/Tree/BPlusTree.php @@ -10,19 +10,51 @@ use KaririCode\DataStructure\Tree\BPlusTreeNode\BPlusTreeLeafNode; use KaririCode\DataStructure\Tree\BPlusTreeNode\BPlusTreeNode; +/** + * BPlusTree is an implementation of a B+ Tree data structure. + * + * The B+ Tree is a self-balancing tree data structure that maintains sorted data + * and allows for efficient insertion, deletion, and search operations. It is commonly + * used in databases and file systems. + * + * ### Complexity Analysis: + * - **Time Complexity**: + * - Insertion: O(log n) + * - Deletion: O(log n) + * - Search: O(log n) + * - Range Search: O(log n + k), where k is the number of elements in the range + * - **Space Complexity**: + * - Space: O(n) + * + * The B+ Tree provides better space utilization and supports range queries efficiently. + * It is optimized for systems that read and write large blocks of data. + * + * @category Trees + * + * @author Walmir Silva + * @license MIT + * + * @see https://kariricode.org/ + */ class BPlusTree implements BPlusTreeCollection { - private int $order; - private ?BPlusTreeNode $root; + private ?BPlusTreeNode $root = null; + private BPlusTreeSearcher $searcher; private int $size = 0; - public function __construct(int $order) + public function __construct(private int $order) { if ($order < 3) { - throw new \InvalidArgumentException("Order must be at least 3"); + throw new \InvalidArgumentException('Order must be at least 3'); } + $this->searcher = new BPlusTreeSearcher(); $this->order = $order; - $this->root = null; + $this->root = new BPlusTreeLeafNode($order); + } + + public function getRoot(): ?BPlusTreeNode + { + return $this->root; } public function add(mixed $element): void @@ -30,33 +62,26 @@ public function add(mixed $element): void $this->insert($element, $element); } - public function insert(int $index, mixed $value): void + public function insert(int $key, mixed $value): void { - if ($this->root === null) { - $this->root = new BPlusTreeLeafNode($this->order); - } - - $this->root = $this->root->insert($index, $value); - - if ($this->root->isFull()) { - $newRoot = new BPlusTreeInternalNode($this->order); - $newRoot->children[] = $this->root; - $this->root = $newRoot->split(); + $newRoot = $this->root->insert($key, $value); + if ($newRoot !== $this->root) { + $this->root = $newRoot; } - $this->size++; + ++$this->size; } public function remove(mixed $element): bool { - if ($this->root === null) { + if (null === $this->root) { return false; } $result = $this->root->remove($element); if ($result) { - $this->size--; - if ($this->root instanceof BPlusTreeInternalNode && count($this->root->keys) === 0) { + --$this->size; + if ($this->root instanceof BPlusTreeInternalNode && 0 === count($this->root->keys)) { $this->root = $this->root->children[0]; } } @@ -70,80 +95,49 @@ public function clear(): void $this->size = 0; } - public function contains(mixed $element): bool + public function find(mixed $element): mixed { - return $this->find($element) !== null; + return $this->searcher->find($this, $element); } - public function find(mixed $element): mixed + public function contains(mixed $element): bool { - if ($this->root === null) { - return null; - } - if (is_int($element)) { - return $this->root->search($element); - } else { - return $this->searchByValue($element); - } + return null !== $this->find($element); } - private function searchByValue(mixed $value): ?int + public function rangeSearch(mixed $start, mixed $end): array { - $current = $this->getLeftmostLeaf(); - while ($current !== null) { - foreach ($current->values as $index => $nodeValue) { - if ($nodeValue === $value) { - return $current->keys[$index]; - } - } - $current = $current->next; - } - return null; + return $this->searcher->rangeSearch($this, $start, $end); } - public function get(int $index): mixed + public function get(int $key): mixed { - if ($index < 0 || $index >= $this->size) { - throw new \OutOfRangeException("Index out of range"); - } - - $current = $this->root; - while ($current instanceof BPlusTreeInternalNode) { - $current = $current->children[0]; - } - - /** @var BPlusTreeLeafNode $current */ - for ($i = 0; $i < $index; $i++) { - $current = $current->next; - if ($current === null) { - throw new \OutOfRangeException("Index out of range"); - } + $value = $this->find($key); + if (null !== $value) { + return $value; + } else { + throw new \OutOfRangeException('Key not found'); } - - return $current->values[0]; } - public function set(int $index, mixed $element): void + public function set(int $key, mixed $value): void { - if ($index < 0 || $index >= $this->size) { - throw new \OutOfRangeException("Index out of range"); - } - - $current = $this->getLeftmostLeaf(); - $currentIndex = 0; - - while ($current !== null) { - for ($i = 0; $i < count($current->keys); $i++) { - if ($current->keys[$i] == $index) { - $current->values[$i] = $element; - return; - } - $currentIndex++; + $node = $this->root; + while ($node instanceof BPlusTreeInternalNode) { + $index = 0; + while ($index < count($node->keys) && $key >= $node->keys[$index]) { + ++$index; } - $current = $current->next; + $node = $node->children[$index]; } - throw new \OutOfRangeException("Index not found"); + /** @var BPlusTreeLeafNode $node */ + $index = array_search($key, $node->keys); + if (false !== $index) { + $node->values[$index] = $value; + } else { + throw new \OutOfRangeException('Key not found'); + } } public function size(): int @@ -155,10 +149,11 @@ public function getItems(): array { $items = []; $current = $this->getLeftmostLeaf(); - while ($current !== null) { + while (null !== $current) { $items = array_merge($items, $current->values); $current = $current->next; } + return $items; } @@ -174,49 +169,18 @@ public function getOrder(): int return $this->order; } - // In BPlusTree.php - - public function rangeSearch(mixed $start, mixed $end): array - { - $result = []; - $current = $this->root; - - // Find the leaf node where the range starts - while ($current instanceof BPlusTreeInternalNode) { - $i = 0; - while ($i < count($current->keys) && $start > $current->keys[$i]) { - $i++; - } - $current = $current->children[$i]; - } - - // Collect all values in the range - /** @var BPlusTreeLeafNode $current */ - while ($current !== null) { - foreach ($current->values as $key => $value) { - if ($current->keys[$key] >= $start && $current->keys[$key] <= $end) { - $result[] = $value; - } - if ($current->keys[$key] > $end) { - return $result; - } - } - $current = $current->next; - } - - return $result; - } - public function getMinimum(): mixed { $leftmostLeaf = $this->getLeftmostLeaf(); - return $leftmostLeaf !== null ? $leftmostLeaf->values[0] : null; + + return null !== $leftmostLeaf ? $leftmostLeaf->values[0] : null; } public function getMaximum(): mixed { $rightmostLeaf = $this->getRightmostLeaf(); - return $rightmostLeaf !== null ? $rightmostLeaf->values[count($rightmostLeaf->values) - 1] : null; + + return null !== $rightmostLeaf ? $rightmostLeaf->values[count($rightmostLeaf->values) - 1] : null; } public function balance(): void @@ -226,9 +190,18 @@ public function balance(): void $this->checkBalance($this->root); } + public function isBalanced(): bool + { + if (null === $this->root) { + return true; + } + + return false !== $this->checkBalance($this->root); + } + private function checkBalance(?BPlusTreeNode $node): int { - if ($node === null) { + if (null === $node) { return 0; } @@ -236,14 +209,13 @@ private function checkBalance(?BPlusTreeNode $node): int return 1; } - /** @var BPlusTreeInternalNode $node */ $height = $this->checkBalance($node->children[0]); - for ($i = 1; $i < count($node->children); $i++) { + for ($i = 1; $i < count($node->children); ++$i) { $childHeight = $this->checkBalance($node->children[$i]); if ($childHeight !== $height) { - throw new \RuntimeException("B+ Tree is not balanced"); + throw new \RuntimeException('B+ Tree is not balanced'); } } @@ -262,10 +234,10 @@ private function checkSorted(): void $current = $this->getLeftmostLeaf(); $prev = null; - while ($current !== null) { + while (null !== $current) { foreach ($current->values as $value) { - if ($prev !== null && $value < $prev) { - throw new \RuntimeException("B+ Tree is not sorted"); + if (null !== $prev && $value < $prev) { + throw new \RuntimeException('B+ Tree is not sorted'); } $prev = $value; } @@ -279,6 +251,7 @@ private function getLeftmostLeaf(): ?BPlusTreeLeafNode while ($current instanceof BPlusTreeInternalNode) { $current = $current->children[0]; } + return $current; } @@ -288,6 +261,40 @@ private function getRightmostLeaf(): ?BPlusTreeLeafNode while ($current instanceof BPlusTreeInternalNode) { $current = $current->children[count($current->children) - 1]; } + return $current; } + + public function visualTreeStructure(): string + { + if (null === $this->root) { + return 'Empty tree'; + } + + return $this->visualizeNode($this->root); + } + + private function visualizeNode(BPlusTreeNode $node, int $depth = 0): string + { + $indent = str_repeat(' ', $depth); + $output = ''; + + if ($node instanceof BPlusTreeInternalNode) { + $output .= $indent . "Internal Node:\n"; + $output .= $indent . ' Keys: ' . implode(', ', $node->keys) . "\n"; + foreach ($node->children as $index => $child) { + $output .= $indent . ' Child ' . ($index + 1) . ":\n"; + $output .= $this->visualizeNode($child, $depth + 2); + } + } elseif ($node instanceof BPlusTreeLeafNode) { + $output .= $indent . "Leaf Node:\n"; + $output .= $indent . ' Keys: ' . implode(', ', $node->keys) . "\n"; + $output .= $indent . ' Values: ' . implode(', ', $node->values) . "\n"; + if ($node->next) { + $output .= $indent . ' Next Leaf -> [' . implode(', ', $node->next->keys) . "]\n"; + } + } + + return $output; + } } diff --git a/src/Tree/BPlusTreeNode/BPlusTreeInternalNode.php b/src/Tree/BPlusTreeNode/BPlusTreeInternalNode.php index f5a4abf..edf8924 100644 --- a/src/Tree/BPlusTreeNode/BPlusTreeInternalNode.php +++ b/src/Tree/BPlusTreeNode/BPlusTreeInternalNode.php @@ -4,27 +4,32 @@ namespace KaririCode\DataStructure\Tree\BPlusTreeNode; +/** + * BPlusTreeInternalNode represents an internal node in a B+ Tree. + * Internal nodes contain keys and children pointers. + * + * @category Trees + * + * @author Walmir Silva + * @license MIT + * + * @see https://kariricode.org/ + */ class BPlusTreeInternalNode extends BPlusTreeNode { public array $keys = []; public array $children = []; - public function insert(int $key, mixed $value): BPlusTreeNode + public function insert(int $key, $value): BPlusTreeNode { - $insertionIndex = $this->findInsertionIndex($key); - $this->children[$insertionIndex] = $this->children[$insertionIndex]->insert($key, $value); - - if ($this->children[$insertionIndex] instanceof BPlusTreeInternalNode) { - $this->keys = array_merge( - array_slice($this->keys, 0, $insertionIndex), - $this->children[$insertionIndex]->keys, - array_slice($this->keys, $insertionIndex) - ); - $this->children = array_merge( - array_slice($this->children, 0, $insertionIndex), - $this->children[$insertionIndex]->children, - array_slice($this->children, $insertionIndex + 1) - ); + $index = $this->findIndex($key); + $child = $this->children[$index]; + $newChild = $child->insert($key, $value); + + if ($newChild !== $child) { + // Insert the new key and child into the current node + array_splice($this->keys, $index, 0, [$newChild->keys[0]]); + array_splice($this->children, $index + 1, 0, [$newChild]); } if ($this->isFull()) { @@ -34,19 +39,56 @@ public function insert(int $key, mixed $value): BPlusTreeNode return $this; } - public function remove(mixed $key): bool + private function findIndex(int $key): int { - $index = $this->findInsertionIndex($key); - return $this->children[$index]->remove($key); + $index = 0; + while ($index < count($this->keys) && $key >= $this->keys[$index]) { + ++$index; + } + + return $index; } + private function split(): BPlusTreeInternalNode + { + $order = $this->order; + $middleKeyIndex = (int) (($order - 1) / 2); + $middleKey = $this->keys[$middleKeyIndex]; + + // Create left and right nodes + $leftNode = new BPlusTreeInternalNode($order); + $rightNode = new BPlusTreeInternalNode($order); + + // Left node keys and children + $leftNode->keys = array_slice($this->keys, 0, $middleKeyIndex); + $leftNode->children = array_slice($this->children, 0, $middleKeyIndex + 1); + + // Right node keys and children + $rightNode->keys = array_slice($this->keys, $middleKeyIndex + 1); + $rightNode->children = array_slice($this->children, $middleKeyIndex + 1); + + // Create a new parent node and promote the middle key + $parent = new BPlusTreeInternalNode($order); + $parent->keys = [$middleKey]; + $parent->children = [$leftNode, $rightNode]; + + return $parent; + } public function search(mixed $key): mixed { $index = $this->findInsertionIndex($key); + return $this->children[$index]->search($key); } + public function remove(mixed $key): bool + { + $index = $this->findInsertionIndex($key); + + return $this->children[$index]->remove($key); + } + private function findInsertionIndex(mixed $key): int { $left = 0; @@ -63,21 +105,4 @@ private function findInsertionIndex(mixed $key): int return $left; } - - public function split(): BPlusTreeInternalNode - { - $middle = (int)($this->order / 2); - - $newNode = new BPlusTreeInternalNode($this->order); - $newNode->keys = array_splice($this->keys, $middle + 1); - $newNode->children = array_splice($this->children, $middle + 1); - - $parent = new BPlusTreeInternalNode($this->order); - $parent->keys[] = $this->keys[$middle]; - $parent->children = [$this, $newNode]; - - array_pop($this->keys); - - return $parent; - } } diff --git a/src/Tree/BPlusTreeNode/BPlusTreeLeafNode.php b/src/Tree/BPlusTreeNode/BPlusTreeLeafNode.php index 76a5cc9..6025de2 100644 --- a/src/Tree/BPlusTreeNode/BPlusTreeLeafNode.php +++ b/src/Tree/BPlusTreeNode/BPlusTreeLeafNode.php @@ -4,17 +4,23 @@ namespace KaririCode\DataStructure\Tree\BPlusTreeNode; +/** + * BPlusTreeLeafNode represents a leaf node in a B+ Tree. + * Leaf nodes contain keys, values, and pointers to the next leaf node. + * + * @category Data Structures + */ class BPlusTreeLeafNode extends BPlusTreeNode { public array $values = []; public ?BPlusTreeLeafNode $next = null; - public function insert(int $key, mixed $value): BPlusTreeNode + public function insert(int $key, $value): BPlusTreeNode { - $insertionIndex = $this->findInsertionIndex($key); + $index = $this->findIndex($key); - array_splice($this->keys, $insertionIndex, 0, [$key]); - array_splice($this->values, $insertionIndex, 0, [$value]); + array_splice($this->keys, $index, 0, [$key]); + array_splice($this->values, $index, 0, [$value]); if ($this->isFull()) { return $this->split(); @@ -23,24 +29,58 @@ public function insert(int $key, mixed $value): BPlusTreeNode return $this; } + private function findIndex(int $key): int + { + $index = 0; + while ($index < count($this->keys) && $key > $this->keys[$index]) { + ++$index; + } + + return $index; + } + + private function split(): BPlusTreeInternalNode + { + $order = $this->order; + $middleIndex = (int) ($order / 2); + $newNode = new BPlusTreeLeafNode($order); + + $newNode->keys = array_slice($this->keys, $middleIndex); + $newNode->values = array_slice($this->values, $middleIndex); + + $this->keys = array_slice($this->keys, 0, $middleIndex); + $this->values = array_slice($this->values, 0, $middleIndex); + + $newNode->next = $this->next; + $this->next = $newNode; + + $parent = new BPlusTreeInternalNode($order); + $parent->keys = [$newNode->keys[0]]; + $parent->children = [$this, $newNode]; + + return $parent; + } + public function remove(mixed $key): bool { $index = $this->findInsertionIndex($key); if ($index < count($this->keys) && $this->keys[$index] === $key) { array_splice($this->keys, $index, 1); array_splice($this->values, $index, 1); + return true; } + return false; } - public function search(mixed $key): mixed { $index = $this->findInsertionIndex($key); if ($index < count($this->keys) && $this->keys[$index] === $key) { return $this->values[$index]; } + return null; } @@ -60,21 +100,4 @@ private function findInsertionIndex(mixed $key): int return $left; } - - private function split(): BPlusTreeInternalNode - { - $middle = (int)($this->order / 2); - - $newNode = new BPlusTreeLeafNode($this->order); - $newNode->keys = array_splice($this->keys, $middle); - $newNode->values = array_splice($this->values, $middle); - $newNode->next = $this->next; - $this->next = $newNode; - - $parent = new BPlusTreeInternalNode($this->order); - $parent->keys[] = $newNode->keys[0]; - $parent->children = [$this, $newNode]; - - return $parent; - } } diff --git a/src/Tree/BPlusTreeNode/BPlusTreeNode.php b/src/Tree/BPlusTreeNode/BPlusTreeNode.php index 81be6dd..22093d9 100644 --- a/src/Tree/BPlusTreeNode/BPlusTreeNode.php +++ b/src/Tree/BPlusTreeNode/BPlusTreeNode.php @@ -4,12 +4,25 @@ namespace KaririCode\DataStructure\Tree\BPlusTreeNode; +/** + * BPlusTreeNode is an abstract class representing a node in a B+ Tree. + * It contains common properties and methods for both internal and leaf nodes. + * + * @category Trees + * + * @author Walmir Silva + * @license MIT + * + * @see https://kariricode.org/ + */ abstract class BPlusTreeNode { public array $keys = []; + protected int $order; - public function __construct(protected int $order) + public function __construct(int $order) { + $this->order = $order; } public function isFull(): bool @@ -18,6 +31,8 @@ public function isFull(): bool } abstract public function insert(int $key, mixed $value): BPlusTreeNode; + abstract public function remove(mixed $key): bool; + abstract public function search(mixed $input): mixed; } diff --git a/src/Tree/BPlusTreeSearcher.php b/src/Tree/BPlusTreeSearcher.php new file mode 100644 index 0000000..f3526b0 --- /dev/null +++ b/src/Tree/BPlusTreeSearcher.php @@ -0,0 +1,135 @@ + + * @license MIT + * + * @see https://kariricode.org/ + */ +class BPlusTreeSearcher +{ + public function find(BPlusTree $tree, mixed $element): mixed + { + $root = $tree->getRoot(); + if (null === $root) { + return null; + } + + return is_int($element) ? + $this->search($root, $element) : + $this->searchByValue($root, $element); + } + + private function search(BPlusTreeNode $node, int $key): mixed + { + if ($node instanceof BPlusTreeLeafNode) { + $index = array_search($key, $node->keys, true); + + return false !== $index ? $node->values[$index] : null; + } + + /** @var BPlusTreeInternalNode $node */ + $index = 0; + while ($index < count($node->keys) && $key >= $node->keys[$index]) { + ++$index; + } + + return $this->search($node->children[$index], $key); + } + + private function searchByValue(BPlusTreeNode $root, mixed $value): ?int + { + $leafNode = $this->findFirstLeafNode($root); + while (null !== $leafNode) { + $result = $this->searchInLeafNode($leafNode, $value); + if (null !== $result) { + return $result; + } + $leafNode = $leafNode->next; + } + + return null; + } + + private function findFirstLeafNode(BPlusTreeNode $node): ?BPlusTreeLeafNode + { + $current = $node; + while ($current instanceof BPlusTreeInternalNode) { + $current = $current->children[0]; + } + + return $current; + } + + private function searchInLeafNode(BPlusTreeLeafNode $node, mixed $value): ?int + { + $values = $node->values; + $keys = $node->keys; + foreach ($values as $index => $nodeValue) { + if ($this->compareValues($nodeValue, $value)) { + return $keys[$index]; + } + } + + return null; + } + + private function compareValues(mixed $a, mixed $b): bool + { + if (is_object($a) && is_object($b)) { + return $a == $b; // Use loose comparison for objects + } + + return $a === $b; // Use strict comparison for other types + } + + public function rangeSearch(BPlusTree $tree, mixed $start, mixed $end): array + { + $result = []; + $current = $tree->getRoot(); + + // Find the leaf node where the range starts + while ($current instanceof BPlusTreeInternalNode) { + $i = 0; + while ($i < count($current->keys) && $start > $current->keys[$i]) { + ++$i; + } + $current = $current->children[$i]; + } + + // Collect all values in the range + /** @var BPlusTreeLeafNode $current */ + while (null !== $current) { + foreach ($current->values as $key => $value) { + if ($current->keys[$key] >= $start && $current->keys[$key] <= $end) { + $result[] = $value; + } + if ($current->keys[$key] > $end) { + return $result; + } + } + $current = $current->next; + } + + return $result; + } +} diff --git a/tests/Tree/BPlusTreeNode/BPlusTreeInternalNodeTest.php b/tests/Tree/BPlusTreeNode/BPlusTreeInternalNodeTest.php new file mode 100644 index 0000000..2156243 --- /dev/null +++ b/tests/Tree/BPlusTreeNode/BPlusTreeInternalNodeTest.php @@ -0,0 +1,74 @@ +insert(10, 'value10'); + $leafNode1->insert(20, 'value20'); + $leafNode2->insert(30, 'value30'); + $leafNode2->insert(40, 'value40'); + + $internalNode->keys = [30]; + $internalNode->children = [$leafNode1, $leafNode2]; + + /** @var BPlusTreeInternalNode $result */ + $result = $internalNode->insert(25, 'value25'); + + $this->assertInstanceOf(BPlusTreeInternalNode::class, $result); + $this->assertSame([25, 30], $result->keys); + $this->assertCount(3, $result->children); + } + + public function testRemove(): void + { + $internalNode = new BPlusTreeInternalNode(4); + $leafNode1 = new BPlusTreeLeafNode(4); + $leafNode2 = new BPlusTreeLeafNode(4); + + $leafNode1->insert(10, 'value10'); + $leafNode1->insert(20, 'value20'); + $leafNode2->insert(30, 'value30'); + $leafNode2->insert(40, 'value40'); + + $internalNode->keys = [30]; + $internalNode->children = [$leafNode1, $leafNode2]; + + $result = $internalNode->remove(20); + $this->assertTrue($result); + + $this->assertSame([30], $internalNode->keys); + $this->assertCount(2, $internalNode->children); + $this->assertSame([10], $internalNode->children[0]->keys); + } + + public function testSearch(): void + { + $internalNode = new BPlusTreeInternalNode(4); + $leafNode1 = new BPlusTreeLeafNode(4); + $leafNode2 = new BPlusTreeLeafNode(4); + + $leafNode1->insert(10, 'value10'); + $leafNode1->insert(20, 'value20'); + $leafNode2->insert(30, 'value30'); + $leafNode2->insert(40, 'value40'); + + $internalNode->keys = [30]; + $internalNode->children = [$leafNode1, $leafNode2]; + + $result = $internalNode->search(30); + $this->assertSame('value30', $result); + } +} diff --git a/tests/Tree/BPlusTreeNode/BPlusTreeLeafNodeTest.php b/tests/Tree/BPlusTreeNode/BPlusTreeLeafNodeTest.php new file mode 100644 index 0000000..5c53884 --- /dev/null +++ b/tests/Tree/BPlusTreeNode/BPlusTreeLeafNodeTest.php @@ -0,0 +1,92 @@ +insert(10, 'value10'); + $leafNode->insert(20, 'value20'); + + $this->assertSame([10, 20], $leafNode->keys); + $this->assertSame(['value10', 'value20'], $leafNode->values); + } + + public function testInsertDuplicates(): void + { + $leafNode = new BPlusTreeLeafNode(4); + $leafNode->insert(10, 'value10'); + $leafNode->insert(10, 'newValue10'); + + $this->assertSame('newValue10', $leafNode->search(10)); + } + + public function testInsertAndSearch() + { + $order = 4; + $leafNode = new BPlusTreeLeafNode($order); + + // Inserindo chaves e valores + $leafNode->insert(10, 'value10'); + $leafNode->insert(20, 'value20'); + $leafNode->insert(30, 'value30'); + + // Testando busca + $this->assertEquals('value10', $leafNode->search(10)); + $this->assertEquals('value20', $leafNode->search(20)); + $this->assertNull($leafNode->search(40)); + } + + public function testRemove(): void + { + $leafNode = new BPlusTreeLeafNode(4); + $leafNode->insert(10, 'value10'); + $leafNode->insert(20, 'value20'); + + $result = $leafNode->remove(10); + + $this->assertTrue($result); + $this->assertSame([20], $leafNode->keys); + $this->assertSame(['value20'], $leafNode->values); + } + + public function testRemoveNonExistent(): void + { + $leafNode = new BPlusTreeLeafNode(4); + $leafNode->insert(10, 'value10'); + + $result = $leafNode->remove(20); + + $this->assertFalse($result); + $this->assertSame([10], $leafNode->keys); + $this->assertSame(['value10'], $leafNode->values); + } + + public function testSearch(): void + { + $leafNode = new BPlusTreeLeafNode(4); + $leafNode->insert(10, 'value10'); + $leafNode->insert(20, 'value20'); + + $result = $leafNode->search(20); + + $this->assertSame('value20', $result); + } + + public function testSearchNonExistent(): void + { + $leafNode = new BPlusTreeLeafNode(4); + $leafNode->insert(10, 'value10'); + + $result = $leafNode->search(20); + + $this->assertNull($result); + } +} diff --git a/tests/Tree/BPlusTreeTest.php b/tests/Tree/BPlusTreeTest.php index 43ba996..f1f661f 100644 --- a/tests/Tree/BPlusTreeTest.php +++ b/tests/Tree/BPlusTreeTest.php @@ -2,198 +2,135 @@ declare(strict_types=1); -namespace KaririCode\DataStructure\Tests\Tree; - -use KaririCode\Contract\DataStructure\Structural\Collection; use KaririCode\DataStructure\Tree\BPlusTree; use PHPUnit\Framework\TestCase; -final class BPlusTreeTest extends TestCase +class BPlusTreeTest extends TestCase { + public const ORDER = 5; + private BPlusTree $tree; protected function setUp(): void { - parent::setUp(); - $this->tree = new BPlusTree(3); // B+ Tree of order 3 + $this->tree = new BPlusTree(self::ORDER); } - // public function testGetThrowsOutOfRangeException(): void - // { - // $this->expectException(\OutOfRangeException::class); - // $this->tree->get(0); - // } - - // Test insertion and root splitting - public function testInsertAndSearch(): void + public function testInsertAndFind(): void { - $this->tree->insert(1, 'a'); - $this->tree->insert(2, 'b'); - $this->tree->insert(3, 'c'); - - // Test search by key - $this->assertSame('a', $this->tree->find(1)); - $this->assertSame('b', $this->tree->find(2)); - $this->assertSame('c', $this->tree->find(3)); - - // Test search by value - $this->assertSame(1, $this->tree->find('a')); - $this->assertSame(2, $this->tree->find('b')); - $this->assertSame(3, $this->tree->find('c')); - } - + $this->tree->insert(1, 'A'); + $this->tree->insert(2, 'B'); + $this->tree->insert(3, 'C'); - - // Test removal and balancing - public function testRemoveAndBalance(): void - { - $this->tree->insert(1, 'a'); - $this->tree->insert(2, 'b'); - $this->tree->insert(3, 'c'); - $this->tree->remove(2); - - $this->assertFalse($this->tree->contains(2)); - $this->assertTrue($this->tree->contains(1)); - $this->assertTrue($this->tree->contains(3)); + $this->assertEquals('A', $this->tree->find(1)); + $this->assertEquals('B', $this->tree->find(2)); + $this->assertEquals('C', $this->tree->find(3)); } - // Test clear method - public function testClear(): void + public function testInsertAndRemove(): void { - $this->tree->insert(1, 'a'); - $this->tree->insert(2, 'b'); - $this->tree->clear(); + $this->tree->insert(1, 'A'); + $this->tree->insert(2, 'B'); + $this->tree->insert(3, 'C'); - $this->assertSame(0, $this->tree->size()); - $this->assertNull($this->tree->find(1)); + $this->assertTrue($this->tree->remove(2)); $this->assertNull($this->tree->find(2)); - } - - // Test contains method - public function testContains(): void - { - $this->tree->insert(1, 'a'); - $this->tree->insert(2, 'b'); - $this->assertTrue($this->tree->contains(1)); - $this->assertTrue($this->tree->contains(2)); - $this->assertFalse($this->tree->contains(3)); - } + $this->assertTrue($this->tree->remove(1)); + $this->assertNull($this->tree->find(1)); - // Test find method - public function testFind(): void - { - $this->tree->insert(1, 'a'); - $this->assertSame('a', $this->tree->find(1)); - $this->assertNull($this->tree->find(2)); + $this->assertFalse($this->tree->remove(5)); // Testa remoção de chave inexistente } - // Test get method - public function testGet(): void + public function testRangeSearch(): void { - $this->tree->insert(1, 'a'); - $this->tree->insert(2, 'b'); - $this->tree->insert(3, 'c'); + $this->tree->insert(1, 'A'); + $this->tree->insert(2, 'B'); + $this->tree->insert(3, 'C'); + $this->tree->insert(4, 'D'); - $this->assertSame('a', $this->tree->get(0)); - $this->assertSame('b', $this->tree->get(1)); - $this->assertSame('c', $this->tree->get(2)); + $result = $this->tree->rangeSearch(2, 3); + $this->assertEquals(['B', 'C'], $result); } - // Test set method - public function testSet(): void + public function testClearTree(): void { - $this->tree->insert(1, 'a'); - $this->tree->insert(2, 'b'); - $this->tree->set(1, 'new-a'); + $this->tree->insert(1, 'A'); + $this->tree->insert(2, 'B'); - $this->assertSame('new-a', $this->tree->find(1)); - $this->assertSame('b', $this->tree->find(2)); - } - - // Test size method - public function testSize(): void - { - $this->assertSame(0, $this->tree->size()); - $this->tree->insert(1, 'a'); - $this->assertSame(1, $this->tree->size()); + $this->tree->clear(); + $this->assertNull($this->tree->getRoot()); + $this->assertEquals(0, $this->tree->size()); } - // Test getItems method public function testGetItems(): void { - $this->tree->insert(1, 'a'); - $this->tree->insert(2, 'b'); - $this->tree->insert(3, 'c'); + $this->tree->insert(1, 'A'); + $this->tree->insert(2, 'B'); + $this->tree->insert(3, 'C'); - $this->assertSame(['a', 'b', 'c'], $this->tree->getItems()); + $items = $this->tree->getItems(); + $this->assertEquals(['A', 'B', 'C'], $items); } - // Test addAll method - public function testAddAll(): void - { - $collection = $this->createMock(Collection::class); - $collection->method('getItems')->willReturn([1, 2, 3]); - - $this->tree->addAll($collection); - - $this->assertSame(3, $this->tree->size()); - } - - // Test getOrder method public function testGetOrder(): void { - $this->assertSame(3, $this->tree->getOrder()); + $this->assertEquals(self::ORDER, $this->tree->getOrder()); } - // Test rangeSearch method - public function testRangeSearch(): void + public function testGetMinimumAndMaximum(): void { - $this->tree->insert(1, 'a'); - $this->tree->insert(2, 'b'); - $this->tree->insert(3, 'c'); + $this->tree->insert(5, 'E'); + $this->tree->insert(1, 'A'); + $this->tree->insert(3, 'C'); - $this->assertSame(['a', 'b'], $this->tree->rangeSearch(1, 2)); + $this->assertEquals('A', $this->tree->getMinimum()); + $this->assertEquals('E', $this->tree->getMaximum()); } - // Test getMinimum method - public function testGetMinimum(): void - { - $this->tree->insert(2, 'b'); - $this->tree->insert(1, 'a'); - $this->tree->insert(3, 'c'); - - $this->assertSame('a', $this->tree->getMinimum()); - } + // public function testTreeBalance(): void + // { + // $this->tree->insert(1, "A"); + // $this->tree->insert(2, "B"); + // $this->tree->insert(3, "C"); + // $this->tree->insert(4, "D"); + // $this->tree->insert(5, "E"); + // $this->tree->insert(6, "F"); + // $this->tree->insert(7, "G"); + // $this->tree->insert(8, "H"); + + // $this->assertTrue($this->tree->isBalanced(), "B+ Tree is not balanced after insertions"); + // } - // Test getMaximum method - public function testGetMaximum(): void + public function testSetAndGetByIndex(): void { - $this->tree->insert(1, 'a'); - $this->tree->insert(3, 'c'); - $this->tree->insert(2, 'b'); + $this->tree->insert(1, 'A'); + $this->tree->insert(2, 'B'); + $this->tree->insert(3, 'C'); - $this->assertSame('c', $this->tree->getMaximum()); + $this->tree->set(1, 'Z'); + $this->assertEquals('Z', $this->tree->get(1)); } - // Test balance method - public function testBalance(): void + public function testVisualTreeStructure(): void { - $this->tree->insert(1, 'a'); - $this->tree->insert(2, 'b'); - $this->tree->balance(); + $this->tree->insert(1, 'A'); + $this->tree->insert(2, 'B'); + $this->tree->insert(3, 'C'); + $this->tree->insert(4, 'D'); + $this->tree->insert(5, 'E'); - $this->assertSame(['a', 'b'], $this->tree->getItems()); + $visual = $this->tree->visualTreeStructure(); + $this->assertStringContainsString('Leaf Node', $visual); + $this->assertStringContainsString('Internal Node', $visual); } - // Test sort method - public function testSort(): void + public function testExceptions(): void { - $this->tree->insert(3, 'c'); - $this->tree->insert(1, 'a'); - $this->tree->insert(2, 'b'); - $this->tree->sort(); + $this->expectException(OutOfRangeException::class); + $this->tree->get(99); // Tenta acessar um índice fora do intervalo - $this->assertSame(['a', 'b', 'c'], $this->tree->getItems()); + $this->expectException(InvalidArgumentException::class); + new BPlusTree(2); // Ordem menor que 3 deve lançar exceção } } diff --git a/tests/bplus_tree_example.php b/tests/bplus_tree_example.php new file mode 100644 index 0000000..542e42b --- /dev/null +++ b/tests/bplus_tree_example.php @@ -0,0 +1,383 @@ +n = 0; + $this->key = array_fill(0, 2 * $t - 1, 0); + $this->child = array_fill(0, 2 * $t, null); + $this->leaf = true; + } +} + +class BTree +{ + private int $T; + private ?Node $root; + + public function __construct(int $t) + { + $this->T = $t; + $this->root = new Node($t); + $this->root->n = 0; + $this->root->leaf = true; + } + + public function insert(int $k): void + { + if ($this->root->n == 2 * $this->T - 1) { + $s = new Node($this->T); + $s->leaf = false; + $s->child[0] = $this->root; + $this->splitChild($s, 0, $this->root); + $this->insertNonFull($s, $k); + $this->root = $s; + } else { + $this->insertNonFull($this->root, $k); + } + } + + private function insertNonFull(Node $x, int $k): void + { + $i = $x->n - 1; + + if ($x->leaf) { + while ($i >= 0 && $k < $x->key[$i]) { + $x->key[$i + 1] = $x->key[$i]; + --$i; + } + $x->key[$i + 1] = $k; + $x->n = $x->n + 1; + } else { + while ($i >= 0 && $k < $x->key[$i]) { + --$i; + } + ++$i; + if ($x->child[$i]->n == 2 * $this->T - 1) { + $this->splitChild($x, $i, $x->child[$i]); + if ($k > $x->key[$i]) { + ++$i; + } + } + $this->insertNonFull($x->child[$i], $k); + } + } + + private function splitChild(Node $x, int $i, Node $y): void + { + $z = new Node($this->T); + $z->leaf = $y->leaf; + $z->n = $this->T - 1; + + for ($j = 0; $j < $this->T - 1; ++$j) { + $z->key[$j] = $y->key[$j + $this->T]; + } + + if (! $y->leaf) { + for ($j = 0; $j < $this->T; ++$j) { + $z->child[$j] = $y->child[$j + $this->T]; + } + } + + $y->n = $this->T - 1; + + for ($j = $x->n; $j >= $i + 1; --$j) { + $x->child[$j + 1] = $x->child[$j]; + } + + $x->child[$i + 1] = $z; + + for ($j = $x->n - 1; $j >= $i; --$j) { + $x->key[$j + 1] = $x->key[$j]; + } + + $x->key[$i] = $y->key[$this->T - 1]; + $x->n = $x->n + 1; + } + + public function remove(int $k): void + { + if (! $this->root) { + return; + } + + $this->removeFromNode($this->root, $k); + + if (0 == $this->root->n) { + if ($this->root->leaf) { + $this->root = null; + } else { + $this->root = $this->root->child[0]; + } + } + } + + private function removeFromNode(Node $x, int $k): void + { + $idx = $this->findKey($x, $k); + + if ($idx < $x->n && $x->key[$idx] == $k) { + if ($x->leaf) { + $this->removeFromLeaf($x, $idx); + } else { + $this->removeFromNonLeaf($x, $idx); + } + } else { + if ($x->leaf) { + return; + } + + $flag = ($idx == $x->n); + + if ($x->child[$idx]->n < $this->T) { + $this->fill($x, $idx); + } + + if ($flag && $idx > $x->n) { + $this->removeFromNode($x->child[$idx - 1], $k); + } else { + $this->removeFromNode($x->child[$idx], $k); + } + } + } + + private function removeFromLeaf(Node $x, int $idx): void + { + for ($i = $idx + 1; $i < $x->n; ++$i) { + $x->key[$i - 1] = $x->key[$i]; + } + --$x->n; + } + + private function removeFromNonLeaf(Node $x, int $idx): void + { + $k = $x->key[$idx]; + + if ($x->child[$idx]->n >= $this->T) { + $pred = $this->getPred($x, $idx); + $x->key[$idx] = $pred; + $this->removeFromNode($x->child[$idx], $pred); + } elseif ($x->child[$idx + 1]->n >= $this->T) { + $succ = $this->getSucc($x, $idx); + $x->key[$idx] = $succ; + $this->removeFromNode($x->child[$idx + 1], $succ); + } else { + $this->merge($x, $idx); + $this->removeFromNode($x->child[$idx], $k); + } + } + + private function getPred(Node $x, int $idx): int + { + $cur = $x->child[$idx]; + while (! $cur->leaf) { + $cur = $cur->child[$cur->n]; + } + + return $cur->key[$cur->n - 1]; + } + + private function getSucc(Node $x, int $idx): int + { + $cur = $x->child[$idx + 1]; + while (! $cur->leaf) { + $cur = $cur->child[0]; + } + + return $cur->key[0]; + } + + private function fill(Node $x, int $idx): void + { + if (0 != $idx && $x->child[$idx - 1]->n >= $this->T) { + $this->borrowFromPrev($x, $idx); + } elseif ($idx != $x->n && $x->child[$idx + 1]->n >= $this->T) { + $this->borrowFromNext($x, $idx); + } else { + if ($idx != $x->n) { + $this->merge($x, $idx); + } else { + $this->merge($x, $idx - 1); + } + } + } + + private function borrowFromPrev(Node $x, int $idx): void + { + $child = $x->child[$idx]; + $sibling = $x->child[$idx - 1]; + + for ($i = $child->n - 1; $i >= 0; --$i) { + $child->key[$i + 1] = $child->key[$i]; + } + + if (! $child->leaf) { + for ($i = $child->n; $i >= 0; --$i) { + $child->child[$i + 1] = $child->child[$i]; + } + } + + $child->key[0] = $x->key[$idx - 1]; + + if (! $child->leaf) { + $child->child[0] = $sibling->child[$sibling->n]; + } + + $x->key[$idx - 1] = $sibling->key[$sibling->n - 1]; + + ++$child->n; + --$sibling->n; + } + + private function borrowFromNext(Node $x, int $idx): void + { + $child = $x->child[$idx]; + $sibling = $x->child[$idx + 1]; + + $child->key[$child->n] = $x->key[$idx]; + + if (! $child->leaf) { + $child->child[$child->n + 1] = $sibling->child[0]; + } + + $x->key[$idx] = $sibling->key[0]; + + for ($i = 1; $i < $sibling->n; ++$i) { + $sibling->key[$i - 1] = $sibling->key[$i]; + } + + if (! $sibling->leaf) { + for ($i = 1; $i <= $sibling->n; ++$i) { + $sibling->child[$i - 1] = $sibling->child[$i]; + } + } + + ++$child->n; + --$sibling->n; + } + + private function merge(Node $x, int $idx): void + { + $child = $x->child[$idx]; + $sibling = $x->child[$idx + 1]; + + $child->key[$this->T - 1] = $x->key[$idx]; + + for ($i = 0; $i < $sibling->n; ++$i) { + $child->key[$i + $this->T] = $sibling->key[$i]; + } + + if (! $child->leaf) { + for ($i = 0; $i <= $sibling->n; ++$i) { + $child->child[$i + $this->T] = $sibling->child[$i]; + } + } + + for ($i = $idx + 1; $i < $x->n; ++$i) { + $x->key[$i - 1] = $x->key[$i]; + } + + for ($i = $idx + 2; $i <= $x->n; ++$i) { + $x->child[$i - 1] = $x->child[$i]; + } + + $child->n += $sibling->n + 1; + --$x->n; + } + + private function findKey(Node $x, int $k): int + { + $idx = 0; + while ($idx < $x->n && $x->key[$idx] < $k) { + ++$idx; + } + + return $idx; + } + + public function search(int $k): ?Node + { + return $this->searchKeyInNode($this->root, $k); + } + + private function searchKeyInNode(?Node $x, int $k): ?Node + { + if (null === $x) { + return null; + } + + $i = 0; + while ($i < $x->n && $k > $x->key[$i]) { + ++$i; + } + if ($i < $x->n && $k == $x->key[$i]) { + return $x; + } + if ($x->leaf) { + return null; + } + + return $this->searchKeyInNode($x->child[$i], $k); + } + + public function printTree(): void + { + if ($this->root) { + $this->printNode($this->root, 0); + } else { + echo "The tree is empty.\n"; + } + } + + private function printNode(Node $x, int $level): void + { + echo str_repeat(' ', $level); + echo "Level $level: "; + for ($i = 0; $i < $x->n; ++$i) { + echo $x->key[$i] . ' '; + } + echo "\n"; + + if (! $x->leaf) { + for ($i = 0; $i <= $x->n; ++$i) { + $this->printNode($x->child[$i], $level + 1); + } + } + } +} + +// Exemplo de uso +$t = new BTree(3); // Árvore B com grau mínimo 3 +$keys = [10, 20, 5, 6, 12, 30, 7, 17]; + +echo 'Inserting keys: ' . implode(', ', $keys) . "\n"; +foreach ($keys as $key) { + $t->insert($key); +} + +echo "\nInitial B-Tree:\n"; +$t->printTree(); + +echo "\nRemoving key 6:\n"; +$t->remove(6); +$t->printTree(); + +echo "\nRemoving key 30:\n"; +$t->remove(30); +$t->printTree(); + +echo "\nSearching for key 12:\n"; +$result = $t->search(12); +echo $result ? "Found\n" : "Not found\n"; + +echo "\nSearching for key 15:\n"; +$result = $t->search(15); +echo $result ? "Found\n" : "Not found\n"; diff --git a/tests/bplus_tree_example2.php b/tests/bplus_tree_example2.php new file mode 100644 index 0000000..7485ff2 --- /dev/null +++ b/tests/bplus_tree_example2.php @@ -0,0 +1,380 @@ +numKeys = 0; + $this->keys = array_fill(0, 2 * $degree - 1, 0); + $this->children = array_fill(0, 2 * $degree, null); + $this->isLeaf = true; + } +} + +class BTree +{ + private int $degree; + private ?BTreeNode $root; + + public function __construct(int $degree) + { + $this->degree = $degree; + $this->root = new BTreeNode($degree); + $this->root->numKeys = 0; + $this->root->isLeaf = true; + } + + public function insert(int $key): void + { + if ($this->root->numKeys === 2 * $this->degree - 1) { + $newRoot = new BTreeNode($this->degree); + $newRoot->isLeaf = false; + $newRoot->children[0] = $this->root; + $this->splitChild($newRoot, 0, $this->root); + $this->insertNonFull($newRoot, $key); + $this->root = $newRoot; + } else { + $this->insertNonFull($this->root, $key); + } + } + + private function insertNonFull(BTreeNode $node, int $key): void + { + $index = $node->numKeys - 1; + + if ($node->isLeaf) { + while ($index >= 0 && $key < $node->keys[$index]) { + $node->keys[$index + 1] = $node->keys[$index]; + --$index; + } + $node->keys[$index + 1] = $key; + ++$node->numKeys; + } else { + while ($index >= 0 && $key < $node->keys[$index]) { + --$index; + } + ++$index; + if ($node->children[$index]->numKeys === 2 * $this->degree - 1) { + $this->splitChild($node, $index, $node->children[$index]); + if ($key > $node->keys[$index]) { + ++$index; + } + } + $this->insertNonFull($node->children[$index], $key); + } + } + + private function splitChild(BTreeNode $parentNode, int $index, BTreeNode $fullChildNode): void + { + $newNode = new BTreeNode($this->degree); + $newNode->isLeaf = $fullChildNode->isLeaf; + $newNode->numKeys = $this->degree - 1; + + for ($j = 0; $j < $this->degree - 1; ++$j) { + $newNode->keys[$j] = $fullChildNode->keys[$j + $this->degree]; + } + + if (! $fullChildNode->isLeaf) { + for ($j = 0; $j < $this->degree; ++$j) { + $newNode->children[$j] = $fullChildNode->children[$j + $this->degree]; + } + } + + $fullChildNode->numKeys = $this->degree - 1; + + for ($j = $parentNode->numKeys; $j >= $index + 1; --$j) { + $parentNode->children[$j + 1] = $parentNode->children[$j]; + } + + $parentNode->children[$index + 1] = $newNode; + + for ($j = $parentNode->numKeys - 1; $j >= $index; --$j) { + $parentNode->keys[$j + 1] = $parentNode->keys[$j]; + } + + $parentNode->keys[$index] = $fullChildNode->keys[$this->degree - 1]; + ++$parentNode->numKeys; + } + + public function remove(int $key): void + { + if (! $this->root) { + return; + } + + $this->removeFromNode($this->root, $key); + + if (0 === $this->root->numKeys) { + $this->root = $this->root->isLeaf ? null : $this->root->children[0]; + } + } + + private function removeFromNode(BTreeNode $node, int $key): void + { + $index = $this->findKeyIndex($node, $key); + + if ($index < $node->numKeys && $node->keys[$index] === $key) { + if ($node->isLeaf) { + $this->removeFromLeaf($node, $index); + } else { + $this->removeFromNonLeaf($node, $index); + } + } else { + if ($node->isLeaf) { + return; + } + + $isLastChild = ($index === $node->numKeys); + + if ($node->children[$index]->numKeys < $this->degree) { + $this->fillNode($node, $index); + } + + if ($isLastChild && $index > $node->numKeys) { + $this->removeFromNode($node->children[$index - 1], $key); + } else { + $this->removeFromNode($node->children[$index], $key); + } + } + } + + private function removeFromLeaf(BTreeNode $node, int $index): void + { + for ($i = $index + 1; $i < $node->numKeys; ++$i) { + $node->keys[$i - 1] = $node->keys[$i]; + } + --$node->numKeys; + } + + private function removeFromNonLeaf(BTreeNode $node, int $index): void + { + $key = $node->keys[$index]; + + if ($node->children[$index]->numKeys >= $this->degree) { + $predKey = $this->getPredecessor($node, $index); + $node->keys[$index] = $predKey; + $this->removeFromNode($node->children[$index], $predKey); + } elseif ($node->children[$index + 1]->numKeys >= $this->degree) { + $succKey = $this->getSuccessor($node, $index); + $node->keys[$index] = $succKey; + $this->removeFromNode($node->children[$index + 1], $succKey); + } else { + $this->mergeNodes($node, $index); + $this->removeFromNode($node->children[$index], $key); + } + } + + private function getPredecessor(BTreeNode $node, int $index): int + { + $currentNode = $node->children[$index]; + while (! $currentNode->isLeaf) { + $currentNode = $currentNode->children[$currentNode->numKeys]; + } + + return $currentNode->keys[$currentNode->numKeys - 1]; + } + + private function getSuccessor(BTreeNode $node, int $index): int + { + $currentNode = $node->children[$index + 1]; + while (! $currentNode->isLeaf) { + $currentNode = $currentNode->children[0]; + } + + return $currentNode->keys[0]; + } + + private function fillNode(BTreeNode $parentNode, int $index): void + { + if (0 !== $index && $parentNode->children[$index - 1]->numKeys >= $this->degree) { + $this->borrowFromPrevious($parentNode, $index); + } elseif ($index !== $parentNode->numKeys && $parentNode->children[$index + 1]->numKeys >= $this->degree) { + $this->borrowFromNext($parentNode, $index); + } else { + if ($index !== $parentNode->numKeys) { + $this->mergeNodes($parentNode, $index); + } else { + $this->mergeNodes($parentNode, $index - 1); + } + } + } + + private function borrowFromPrevious(BTreeNode $parentNode, int $index): void + { + $childNode = $parentNode->children[$index]; + $siblingNode = $parentNode->children[$index - 1]; + + for ($i = $childNode->numKeys - 1; $i >= 0; --$i) { + $childNode->keys[$i + 1] = $childNode->keys[$i]; + } + + if (! $childNode->isLeaf) { + for ($i = $childNode->numKeys; $i >= 0; --$i) { + $childNode->children[$i + 1] = $childNode->children[$i]; + } + } + + $childNode->keys[0] = $parentNode->keys[$index - 1]; + + if (! $childNode->isLeaf) { + $childNode->children[0] = $siblingNode->children[$siblingNode->numKeys]; + } + + $parentNode->keys[$index - 1] = $siblingNode->keys[$siblingNode->numKeys - 1]; + + ++$childNode->numKeys; + --$siblingNode->numKeys; + } + + private function borrowFromNext(BTreeNode $parentNode, int $index): void + { + $childNode = $parentNode->children[$index]; + $siblingNode = $parentNode->children[$index + 1]; + + $childNode->keys[$childNode->numKeys] = $parentNode->keys[$index]; + + if (! $childNode->isLeaf) { + $childNode->children[$childNode->numKeys + 1] = $siblingNode->children[0]; + } + + $parentNode->keys[$index] = $siblingNode->keys[0]; + + for ($i = 1; $i < $siblingNode->numKeys; ++$i) { + $siblingNode->keys[$i - 1] = $siblingNode->keys[$i]; + } + + if (! $siblingNode->isLeaf) { + for ($i = 1; $i <= $siblingNode->numKeys; ++$i) { + $siblingNode->children[$i - 1] = $siblingNode->children[$i]; + } + } + + ++$childNode->numKeys; + --$siblingNode->numKeys; + } + + private function mergeNodes(BTreeNode $parentNode, int $index): void + { + $childNode = $parentNode->children[$index]; + $siblingNode = $parentNode->children[$index + 1]; + + $childNode->keys[$this->degree - 1] = $parentNode->keys[$index]; + + for ($i = 0; $i < $siblingNode->numKeys; ++$i) { + $childNode->keys[$i + $this->degree] = $siblingNode->keys[$i]; + } + + if (! $childNode->isLeaf) { + for ($i = 0; $i <= $siblingNode->numKeys; ++$i) { + $childNode->children[$i + $this->degree] = $siblingNode->children[$i]; + } + } + + for ($i = $index + 1; $i < $parentNode->numKeys; ++$i) { + $parentNode->keys[$i - 1] = $parentNode->keys[$i]; + } + + for ($i = $index + 2; $i <= $parentNode->numKeys; ++$i) { + $parentNode->children[$i - 1] = $parentNode->children[$i]; + } + + $childNode->numKeys += $siblingNode->numKeys + 1; + --$parentNode->numKeys; + } + + private function findKeyIndex(BTreeNode $node, int $key): int + { + $index = 0; + while ($index < $node->numKeys && $node->keys[$index] < $key) { + ++$index; + } + + return $index; + } + + public function search(int $key): ?BTreeNode + { + return $this->searchInNode($this->root, $key); + } + + private function searchInNode(?BTreeNode $node, int $key): ?BTreeNode + { + if (null === $node) { + return null; + } + + $index = 0; + while ($index < $node->numKeys && $key > $node->keys[$index]) { + ++$index; + } + if ($index < $node->numKeys && $key === $node->keys[$index]) { + return $node; + } + if ($node->isLeaf) { + return null; + } + + return $this->searchInNode($node->children[$index], $key); + } + + public function printTree(): void + { + if ($this->root) { + $this->printNode($this->root, 0); + } else { + echo "The tree is empty.\n"; + } + } + + private function printNode(BTreeNode $node, int $level): void + { + echo str_repeat(' ', $level); + echo "Level $level: "; + for ($i = 0; $i < $node->numKeys; ++$i) { + echo $node->keys[$i] . ' '; + } + echo "\n"; + + if (! $node->isLeaf) { + for ($i = 0; $i <= $node->numKeys; ++$i) { + $this->printNode($node->children[$i], $level + 1); + } + } + } +} + +// Exemplo de uso +$degree = 3; // Árvore B com grau mínimo 3 +$btree = new BTree($degree); +$keys = [10, 20, 5, 6, 12, 30, 7, 17]; + +echo 'Inserting keys: ' . implode(', ', $keys) . "\n"; +foreach ($keys as $key) { + $btree->insert($key); +} + +echo "\nInitial B-Tree:\n"; +$btree->printTree(); + +echo "\nRemoving key 6:\n"; +$btree->remove(6); +$btree->printTree(); + +echo "\nRemoving key 30:\n"; +$btree->remove(30); +$btree->printTree(); + +echo "\nSearching for key 12:\n"; +$result = $btree->search(12); +echo $result ? "Found\n" : "Not found\n"; + +echo "\nSearching for key 15:\n"; +$result = $btree->search(15); +echo $result ? "Found\n" : "Not found\n"; diff --git a/tests/bplus_tree_example3.php b/tests/bplus_tree_example3.php new file mode 100644 index 0000000..26f1853 --- /dev/null +++ b/tests/bplus_tree_example3.php @@ -0,0 +1,404 @@ +keyCount = 0; + $this->keys = array_fill(0, 2 * $degree - 1, null); + $this->children = array_fill(0, 2 * $degree, null); + $this->isLeaf = true; + } +} + +class BTree +{ + private int $minDegree; + private ?BTreeNode $root; + + public function __construct(int $degree) + { + $this->minDegree = $degree; + $this->root = new BTreeNode($degree); + } + + public function insert(int|float|string $key): void + { + if ($this->root->keyCount === 2 * $this->minDegree - 1) { + $newRoot = new BTreeNode($this->minDegree); + $newRoot->isLeaf = false; + $newRoot->children[0] = $this->root; + $this->splitChild($newRoot, 0, $this->root); + $this->insertNonFull($newRoot, $key); + $this->root = $newRoot; + } else { + $this->insertNonFull($this->root, $key); + } + } + + private function insertNonFull(BTreeNode $node, int|float|string $key): void + { + $i = $node->keyCount - 1; + + if ($node->isLeaf) { + while ($i >= 0 && $key < $node->keys[$i]) { + $node->keys[$i + 1] = $node->keys[$i]; + --$i; + } + $node->keys[$i + 1] = $key; + ++$node->keyCount; + } else { + while ($i >= 0 && $key < $node->keys[$i]) { + --$i; + } + ++$i; + if ($node->children[$i]->keyCount === 2 * $this->minDegree - 1) { + $this->splitChild($node, $i, $node->children[$i]); + if ($key > $node->keys[$i]) { + ++$i; + } + } + $this->insertNonFull($node->children[$i], $key); + } + } + + private function splitChild(BTreeNode $parent, int $index, BTreeNode $fullNode): void + { + $newNode = new BTreeNode($this->minDegree); + $newNode->isLeaf = $fullNode->isLeaf; + $newNode->keyCount = $this->minDegree - 1; + + for ($j = 0; $j < $this->minDegree - 1; ++$j) { + $newNode->keys[$j] = $fullNode->keys[$j + $this->minDegree]; + } + + if (! $fullNode->isLeaf) { + for ($j = 0; $j < $this->minDegree; ++$j) { + $newNode->children[$j] = $fullNode->children[$j + $this->minDegree]; + } + } + + $fullNode->keyCount = $this->minDegree - 1; + + for ($j = $parent->keyCount; $j >= $index + 1; --$j) { + $parent->children[$j + 1] = $parent->children[$j]; + } + + $parent->children[$index + 1] = $newNode; + + for ($j = $parent->keyCount - 1; $j >= $index; --$j) { + $parent->keys[$j + 1] = $parent->keys[$j]; + } + + $parent->keys[$index] = $fullNode->keys[$this->minDegree - 1]; + ++$parent->keyCount; + } + + public function remove(int|float|string $key): void + { + if (! $this->root) { + return; + } + + $this->removeFromNode($this->root, $key); + + if (0 === $this->root->keyCount) { + $this->root = $this->root->isLeaf ? null : $this->root->children[0]; + } + } + + private function removeFromNode(BTreeNode $node, int|float|string $key): void + { + $idx = $this->findKey($node, $key); + + if ($idx < $node->keyCount && $node->keys[$idx] === $key) { + if ($node->isLeaf) { + $this->removeFromLeaf($node, $idx); + } else { + $this->removeFromNonLeaf($node, $idx); + } + } else { + if ($node->isLeaf) { + return; + } + + $flag = ($idx === $node->keyCount); + + if ($node->children[$idx]->keyCount < $this->minDegree) { + $this->fill($node, $idx); + } + + if ($flag && $idx > $node->keyCount) { + $this->removeFromNode($node->children[$idx - 1], $key); + } else { + $this->removeFromNode($node->children[$idx], $key); + } + } + } + + private function removeFromLeaf(BTreeNode $node, int $index): void + { + for ($i = $index + 1; $i < $node->keyCount; ++$i) { + $node->keys[$i - 1] = $node->keys[$i]; + } + --$node->keyCount; + } + + private function removeFromNonLeaf(BTreeNode $node, int $index): void + { + $key = $node->keys[$index]; + + if ($node->children[$index]->keyCount >= $this->minDegree) { + $pred = $this->getPredecessor($node, $index); + $node->keys[$index] = $pred; + $this->removeFromNode($node->children[$index], $pred); + } elseif ($node->children[$index + 1]->keyCount >= $this->minDegree) { + $succ = $this->getSuccessor($node, $index); + $node->keys[$index] = $succ; + $this->removeFromNode($node->children[$index + 1], $succ); + } else { + $this->merge($node, $index); + $this->removeFromNode($node->children[$index], $key); + } + } + + private function getPredecessor(BTreeNode $node, int $index): int|float|string + { + $current = $node->children[$index]; + while (! $current->isLeaf) { + $current = $current->children[$current->keyCount]; + } + + return $current->keys[$current->keyCount - 1]; + } + + private function getSuccessor(BTreeNode $node, int $index): int|float|string + { + $current = $node->children[$index + 1]; + while (! $current->isLeaf) { + $current = $current->children[0]; + } + + return $current->keys[0]; + } + + private function fill(BTreeNode $node, int $index): void + { + if (0 !== $index && $node->children[$index - 1]->keyCount >= $this->minDegree) { + $this->borrowFromPrevious($node, $index); + } elseif ($index !== $node->keyCount && $node->children[$index + 1]->keyCount >= $this->minDegree) { + $this->borrowFromNext($node, $index); + } else { + if ($index !== $node->keyCount) { + $this->merge($node, $index); + } else { + $this->merge($node, $index - 1); + } + } + } + + private function borrowFromPrevious(BTreeNode $node, int $index): void + { + $child = $node->children[$index]; + $sibling = $node->children[$index - 1]; + + for ($i = $child->keyCount - 1; $i >= 0; --$i) { + $child->keys[$i + 1] = $child->keys[$i]; + } + + if (! $child->isLeaf) { + for ($i = $child->keyCount; $i >= 0; --$i) { + $child->children[$i + 1] = $child->children[$i]; + } + } + + $child->keys[0] = $node->keys[$index - 1]; + + if (! $child->isLeaf) { + $child->children[0] = $sibling->children[$sibling->keyCount]; + } + + $node->keys[$index - 1] = $sibling->keys[$sibling->keyCount - 1]; + + ++$child->keyCount; + --$sibling->keyCount; + } + + private function borrowFromNext(BTreeNode $node, int $index): void + { + $child = $node->children[$index]; + $sibling = $node->children[$index + 1]; + + $child->keys[$child->keyCount] = $node->keys[$index]; + + if (! $child->isLeaf) { + $child->children[$child->keyCount + 1] = $sibling->children[0]; + } + + $node->keys[$index] = $sibling->keys[0]; + + for ($i = 1; $i < $sibling->keyCount; ++$i) { + $sibling->keys[$i - 1] = $sibling->keys[$i]; + } + + if (! $sibling->isLeaf) { + for ($i = 1; $i <= $sibling->keyCount; ++$i) { + $sibling->children[$i - 1] = $sibling->children[$i]; + } + } + + ++$child->keyCount; + --$sibling->keyCount; + } + + private function merge(BTreeNode $node, int $index): void + { + $child = $node->children[$index]; + $sibling = $node->children[$index + 1]; + + $child->keys[$this->minDegree - 1] = $node->keys[$index]; + + for ($i = 0; $i < $sibling->keyCount; ++$i) { + $child->keys[$i + $this->minDegree] = $sibling->keys[$i]; + } + + if (! $child->isLeaf) { + for ($i = 0; $i <= $sibling->keyCount; ++$i) { + $child->children[$i + $this->minDegree] = $sibling->children[$i]; + } + } + + for ($i = $index + 1; $i < $node->keyCount; ++$i) { + $node->keys[$i - 1] = $node->keys[$i]; + } + + for ($i = $index + 2; $i <= $node->keyCount; ++$i) { + $node->children[$i - 1] = $node->children[$i]; + } + + $child->keyCount += $sibling->keyCount + 1; + --$node->keyCount; + } + + private function findKey(BTreeNode $node, int|float|string $key): int + { + $index = 0; + while ($index < $node->keyCount && $node->keys[$index] < $key) { + ++$index; + } + + return $index; + } + + public function search(int|float|string $key): ?BTreeNode + { + return $this->searchKeyInNode($this->root, $key); + } + + private function searchKeyInNode(?BTreeNode $node, int|float|string $key): ?BTreeNode + { + if (null === $node) { + return null; + } + + $i = 0; + while ($i < $node->keyCount && $key > $node->keys[$i]) { + ++$i; + } + if ($i < $node->keyCount && $key == $node->keys[$i]) { + return $node; + } + if ($node->isLeaf) { + return null; + } + + return $this->searchKeyInNode($node->children[$i], $key); + } + + public function printTree(): void + { + if ($this->root) { + $this->printNode($this->root, 0); + } else { + echo "The tree is empty.\n"; + } + } + + private function printNode(BTreeNode $node, int $level): void + { + echo str_repeat(' ', $level); + echo "Level $level: "; + for ($i = 0; $i < $node->keyCount; ++$i) { + echo $node->keys[$i] . ' '; + } + echo "\n"; + + if (! $node->isLeaf) { + for ($i = 0; $i <= $node->keyCount; ++$i) { + $this->printNode($node->children[$i], $level + 1); + } + } + } +} + +// Exemplo de uso +$tree = new BTree(3); // Árvore B com grau mínimo 3 +$keys = [10, 20, 5, 6, 12, 30, 7, 17]; + +echo 'Inserting keys: ' . implode(', ', $keys) . "\n"; +foreach ($keys as $key) { + $tree->insert($key); +} + +echo "\nInitial B-Tree:\n"; +$tree->printTree(); + +echo "\nRemoving key 6:\n"; +$tree->remove(6); +$tree->printTree(); + +echo "\nRemoving key 30:\n"; +$tree->remove(30); +$tree->printTree(); + +echo "\nSearching for key 12:\n"; +$result = $tree->search(12); +echo $result ? "Found\n" : "Not found\n"; + +echo "\nSearching for key 15:\n"; +$result = $tree->search(15); +echo $result ? "Found\n" : "Not found\n"; + +$tree = new BTree(3); // Árvore B com grau mínimo 3 +$keys = ['D', 'B', 'A', 'C', 'F', 'E', 'H', 'G']; + +echo 'Inserting keys: ' . implode(', ', $keys) . "\n"; +foreach ($keys as $key) { + $tree->insert($key); +} + +echo "\nInitial B-Tree:\n"; +$tree->printTree(); + +echo "\nRemoving key 'C':\n"; +$tree->remove('C'); +$tree->printTree(); + +echo "\nRemoving key 'F':\n"; +$tree->remove('F'); +$tree->printTree(); + +echo "\nSearching for key 'B':\n"; +$result = $tree->search('B'); +echo $result ? "Found\n" : "Not found\n"; + +echo "\nSearching for key 'Z':\n"; +$result = $tree->search('Z'); +echo $result ? "Found\n" : "Not found\n"; diff --git a/tests/bplus_tree_example4.php b/tests/bplus_tree_example4.php new file mode 100644 index 0000000..810b366 --- /dev/null +++ b/tests/bplus_tree_example4.php @@ -0,0 +1,493 @@ +keyCount = 0; + $this->keys = array_fill(0, 2 * $degree - 1, null); + $this->children = array_fill(0, 2 * $degree, null); + $this->isLeaf = true; + } + + public function isFull(int $degree): bool + { + return $this->keyCount === 2 * $degree - 1; + } + + public function isUnderflow(int $degree): bool + { + return $this->keyCount < $degree; + } + + public function isLeaf(): bool + { + return $this->isLeaf; + } +} + +class BPlusTree +{ + private int $minDegree; + private ?BPlusTreeNode $root; + + public function __construct(int $degree) + { + $this->minDegree = $degree; + $this->root = new BPlusTreeNode($degree); + } + + public function insert(int|float|string $key): void + { + if ($this->isRootFull()) { + $newRoot = new BPlusTreeNode($this->minDegree); + $newRoot->isLeaf = false; + $newRoot->children[0] = $this->root; + $this->splitChild($newRoot, 0, $this->root); + $this->insertNonFull($newRoot, $key); + $this->root = $newRoot; + } else { + $this->insertNonFull($this->root, $key); + } + } + + private function isRootFull(): bool + { + return $this->root->isFull($this->minDegree); + } + + private function insertNonFull(BPlusTreeNode $node, int|float|string $key): void + { + $i = $node->keyCount - 1; + + if ($node->isLeaf) { + while ($i >= 0 && $key < $node->keys[$i]) { + $node->keys[$i + 1] = $node->keys[$i]; + --$i; + } + $node->keys[$i + 1] = $key; + ++$node->keyCount; + } else { + while ($i >= 0 && $key < $node->keys[$i]) { + --$i; + } + ++$i; + if ($node->children[$i]->isFull($this->minDegree)) { + $this->splitChild($node, $i, $node->children[$i]); + if ($key > $node->keys[$i]) { + ++$i; + } + } + $this->insertNonFull($node->children[$i], $key); + } + } + + private function splitChild(BPlusTreeNode $parent, int $index, BPlusTreeNode $fullNode): void + { + $newNode = new BPlusTreeNode($this->minDegree); + $newNode->isLeaf = $fullNode->isLeaf; + $newNode->keyCount = $this->minDegree - 1; + + for ($j = 0; $j < $this->minDegree - 1; ++$j) { + $newNode->keys[$j] = $fullNode->keys[$j + $this->minDegree]; + } + + if (! $fullNode->isLeaf) { + for ($j = 0; $j < $this->minDegree; ++$j) { + $newNode->children[$j] = $fullNode->children[$j + $this->minDegree]; + } + } + + $fullNode->keyCount = $this->minDegree - 1; + + for ($j = $parent->keyCount; $j >= $index + 1; --$j) { + $parent->children[$j + 1] = $parent->children[$j]; + } + + $parent->children[$index + 1] = $newNode; + + for ($j = $parent->keyCount - 1; $j >= $index; --$j) { + $parent->keys[$j + 1] = $parent->keys[$j]; + } + + $parent->keys[$index] = $fullNode->keys[$this->minDegree - 1]; + ++$parent->keyCount; + } + + public function remove(int|float|string $key): void + { + if (! $this->root) { + return; + } + + $this->removeFromNode($this->root, $key); + + if (0 === $this->root->keyCount) { + $this->root = $this->root->isLeaf() ? null : $this->root->children[0]; + } + } + + private function removeFromNode(BPlusTreeNode $node, int|float|string $key): void + { + $idx = $this->findKey($node, $key); + + if ($this->keyExistsInNode($node, $idx, $key)) { + $node->isLeaf() ? $this->removeFromLeaf($node, $idx) : $this->removeFromNonLeaf($node, $idx); + } else { + if ($node->isLeaf()) { + return; + } + + $this->handleChildUnderflow($node, $idx, $key); + } + } + + private function keyExistsInNode(BPlusTreeNode $node, int $idx, int|float|string $key): bool + { + return $idx < $node->keyCount && $node->keys[$idx] === $key; + } + + private function handleChildUnderflow(BPlusTreeNode $node, int $idx, int|float|string $key): void + { + $flag = ($idx === $node->keyCount); + + if ($node->children[$idx]->isUnderflow($this->minDegree)) { + $this->fill($node, $idx); + } + + $this->removeFromNode( + $node->children[$flag && $idx > $node->keyCount ? $idx - 1 : $idx], + $key + ); + } + + private function removeFromLeaf(BPlusTreeNode $node, int $index): void + { + for ($i = $index + 1; $i < $node->keyCount; ++$i) { + $node->keys[$i - 1] = $node->keys[$i]; + } + --$node->keyCount; + } + + private function removeFromNonLeaf(BPlusTreeNode $node, int $index): void + { + $key = $node->keys[$index]; + + if ($node->children[$index]->keyCount >= $this->minDegree) { + $pred = $this->getPredecessor($node, $index); + $node->keys[$index] = $pred; + $this->removeFromNode($node->children[$index], $pred); + } elseif ($node->children[$index + 1]->keyCount >= $this->minDegree) { + $succ = $this->getSuccessor($node, $index); + $node->keys[$index] = $succ; + $this->removeFromNode($node->children[$index + 1], $succ); + } else { + $this->merge($node, $index); + $this->removeFromNode($node->children[$index], $key); + } + } + + private function getPredecessor(BPlusTreeNode $node, int $index): int|float|string + { + $current = $node->children[$index]; + while (! $current->isLeaf) { + $current = $current->children[$current->keyCount]; + } + + return $current->keys[$current->keyCount - 1]; + } + + private function getSuccessor(BPlusTreeNode $node, int $index): int|float|string + { + $current = $node->children[$index + 1]; + while (! $current->isLeaf) { + $current = $current->children[0]; + } + + return $current->keys[0]; + } + + private function fill(BPlusTreeNode $node, int $index): void + { + if (0 !== $index && $node->children[$index - 1]->keyCount >= $this->minDegree) { + $this->borrowFromPrevious($node, $index); + } elseif ($index !== $node->keyCount && $node->children[$index + 1]->keyCount >= $this->minDegree) { + $this->borrowFromNext($node, $index); + } else { + $this->merge($node, $index !== $node->keyCount ? $index : $index - 1); + } + } + + private function borrowFromPrevious(BPlusTreeNode $node, int $index): void + { + $child = $node->children[$index]; + $sibling = $node->children[$index - 1]; + + for ($i = $child->keyCount - 1; $i >= 0; --$i) { + $child->keys[$i + 1] = $child->keys[$i]; + } + + if (! $child->isLeaf) { + for ($i = $child->keyCount; $i >= 0; --$i) { + $child->children[$i + 1] = $child->children[$i]; + } + } + + $child->keys[0] = $node->keys[$index - 1]; + + if (! $child->isLeaf) { + $child->children[0] = $sibling->children[$sibling->keyCount]; + } + + $node->keys[$index - 1] = $sibling->keys[$sibling->keyCount - 1]; + + ++$child->keyCount; + --$sibling->keyCount; + } + + private function borrowFromNext(BPlusTreeNode $node, int $index): void + { + $child = $node->children[$index]; + $sibling = $node->children[$index + 1]; + + $child->keys[$child->keyCount] = $node->keys[$index]; + + if (! $child->isLeaf) { + $child->children[$child->keyCount + 1] = $sibling->children[0]; + } + + $node->keys[$index] = $sibling->keys[0]; + + for ($i = 1; $i < $sibling->keyCount; ++$i) { + $sibling->keys[$i - 1] = $sibling->keys[$i]; + } + + if (! $sibling->isLeaf) { + for ($i = 1; $i <= $sibling->keyCount; ++$i) { + $sibling->children[$i - 1] = $sibling->children[$i]; + } + } + + ++$child->keyCount; + --$sibling->keyCount; + } + + private function merge(BPlusTreeNode $node, int $index): void + { + $child = $node->children[$index]; + $sibling = $node->children[$index + 1]; + + $child->keys[$this->minDegree - 1] = $node->keys[$index]; + + for ($i = 0; $i < $sibling->keyCount; ++$i) { + $child->keys[$i + $this->minDegree] = $sibling->keys[$i]; + } + + if (! $child->isLeaf) { + for ($i = 0; $i <= $sibling->keyCount; ++$i) { + $child->children[$i + $this->minDegree] = $sibling->children[$i]; + } + } + + for ($i = $index + 1; $i < $node->keyCount; ++$i) { + $node->keys[$i - 1] = $node->keys[$i]; + } + + for ($i = $index + 2; $i <= $node->keyCount; ++$i) { + $node->children[$i - 1] = $node->children[$i]; + } + + $child->keyCount += $sibling->keyCount + 1; + --$node->keyCount; + } + + private function findKey(BPlusTreeNode $node, int|float|string $key): int + { + $index = 0; + while ($index < $node->keyCount && $node->keys[$index] < $key) { + ++$index; + } + + return $index; + } + + public function search(int|float|string $key): ?BPlusTreeNode + { + return $this->searchKeyInNode($this->root, $key); + } + + private function searchKeyInNode(?BPlusTreeNode $node, int|float|string $key): ?BPlusTreeNode + { + if (null === $node) { + return null; + } + + $i = 0; + while ($i < $node->keyCount && $key > $node->keys[$i]) { + ++$i; + } + if ($i < $node->keyCount && $key == $node->keys[$i]) { + return $node; + } + if ($node->isLeaf()) { + return null; + } + + return $this->searchKeyInNode($node->children[$i], $key); + } + + public function printTree(): void + { + if ($this->root) { + $this->printNode($this->root, 0); + } else { + echo "The tree is empty.\n"; + } + } + + private function printNode(BPlusTreeNode $node, int $level): void + { + echo str_repeat(' ', $level); + echo "Level $level: "; + for ($i = 0; $i < $node->keyCount; ++$i) { + echo $node->keys[$i] . ' '; + } + echo "\n"; + + if (! $node->isLeaf()) { + for ($i = 0; $i <= $node->keyCount; ++$i) { + $this->printNode($node->children[$i], $level + 1); + } + } + } +} + +// Exemplo de uso +$tree = new BPlusTree(3); // Árvore B com grau mínimo 3 +$keys = [10, 20, 5, 6, 12, 30, 7, 17]; + +echo 'Inserting keys: ' . implode(', ', $keys) . "\n"; +foreach ($keys as $key) { + $tree->insert($key); +} + +echo "\nInitial B-Tree:\n"; +$tree->printTree(); + +echo "\nRemoving key 6:\n"; +$tree->remove(6); +$tree->printTree(); + +echo "\nRemoving key 30:\n"; +$tree->remove(30); +$tree->printTree(); + +echo "\nSearching for key 12:\n"; +$result = $tree->search(12); +echo $result ? "Found\n" : "Not found\n"; + +echo "\nSearching for key 15:\n"; +$result = $tree->search(15); +echo $result ? "Found\n" : "Not found\n"; + +$tree = new BPlusTree(3); // Árvore B com grau mínimo 3 +$keys = ['D', 'B', 'A', 'C', 'F', 'E', 'H', 'G']; + +echo 'Inserting keys: ' . implode(', ', $keys) . "\n"; +foreach ($keys as $key) { + $tree->insert($key); +} + +echo "\nInitial B-Tree:\n"; +$tree->printTree(); + +echo "\nRemoving key 'C':\n"; +$tree->remove('C'); +$tree->printTree(); + +echo "\nRemoving key 'F':\n"; +$tree->remove('F'); +$tree->printTree(); + +echo "\nSearching for key 'B':\n"; +$result = $tree->search('B'); +echo $result ? "Found\n" : "Not found\n"; + +echo "\nSearching for key 'Z':\n"; +$result = $tree->search('Z'); +echo $result ? "Found\n" : "Not found\n"; + +function generateRandomKeys(int $count, int $min = 1, int $max = 1000000): array +{ + $keys = []; + for ($i = 0; $i < $count; ++$i) { + $keys[] = rand($min, $max); + } + + return $keys; +} + +function measurePerformance(int $numKeys, int $iterations): array +{ + $degree = 3; + $insertTimes = []; + $removeTimes = []; + + for ($i = 0; $i < $iterations; ++$i) { + $tree = new BPlusTree($degree); + $keys = generateRandomKeys($numKeys); + + // Medir tempo de inserção + $startInsert = microtime(true); + foreach ($keys as $key) { + $tree->insert($key); + } + $endInsert = microtime(true); + $insertTimes[] = $endInsert - $startInsert; + + // Medir tempo de remoção + $startRemove = microtime(true); + foreach ($keys as $key) { + $tree->remove($key); + } + $endRemove = microtime(true); + $removeTimes[] = $endRemove - $startRemove; + } + + return [ + 'insert' => $insertTimes, + 'remove' => $removeTimes, + ]; +} + +function calculateAverage(array $times): float +{ + return array_sum($times) / count($times); +} + +function testPerformance(array $numKeysList, int $iterations): void +{ + foreach ($numKeysList as $numKeys) { + $times = measurePerformance($numKeys, $iterations); + $avgInsertTime = calculateAverage($times['insert']); + $avgRemoveTime = calculateAverage($times['remove']); + + echo "Número de chaves: $numKeys\n"; + echo 'Inserção - Tempo médio: ' . number_format($avgInsertTime, 6) . " segundos\n"; + echo 'Remoção - Tempo médio: ' . number_format($avgRemoveTime, 6) . " segundos\n"; + echo "-----------------------------------------\n"; + } +} + +// Definir diferentes quantidades de chaves para testar +$numKeysList = [10000, 50000, 100000, 200000, 500000]; // Você pode ajustar esses valores conforme necessário +$iterations = 5; // Número de iterações para cada quantidade de chaves + +testPerformance($numKeysList, $iterations); diff --git a/tests/bplus_tree_example5.php b/tests/bplus_tree_example5.php new file mode 100644 index 0000000..1f56e8a --- /dev/null +++ b/tests/bplus_tree_example5.php @@ -0,0 +1,543 @@ +keyCount = 0; + $this->keys = array_fill(0, 2 * $degree - 1, null); + $this->children = array_fill(0, 2 * $degree, null); + $this->isLeaf = true; + } + + public function isFull(int $degree): bool + { + return $this->keyCount === 2 * $degree - 1; + } + + public function isUnderflow(int $degree): bool + { + return $this->keyCount < $degree; + } + + public function isLeaf(): bool + { + return $this->isLeaf; + } +} + +class BPlusTree +{ + private int $minDegree; + private ?BPlusTreeNode $root; + + public function __construct(int $degree) + { + $this->minDegree = $degree; + $this->root = new BPlusTreeNode($degree); + } + + public function getRoot(): BPlusTreeNode + { + return $this->root; + } + + public function insert(mixed $key): void + { + if ($this->isRootFull()) { + $newRoot = new BPlusTreeNode($this->minDegree); + $newRoot->isLeaf = false; + $newRoot->children[0] = $this->root; + $this->splitChild($newRoot, 0, $this->root); + $this->insertNonFull($newRoot, $key); + $this->root = $newRoot; + } else { + $this->insertNonFull($this->root, $key); + } + } + + private function isRootFull(): bool + { + return $this->root->isFull($this->minDegree); + } + + private function insertNonFull(BPlusTreeNode $node, mixed $key): void + { + $i = $node->keyCount - 1; + + if ($node->isLeaf) { + while ($i >= 0 && $key < $node->keys[$i]) { + $node->keys[$i + 1] = $node->keys[$i]; + --$i; + } + $node->keys[$i + 1] = $key; + ++$node->keyCount; + } else { + while ($i >= 0 && $key < $node->keys[$i]) { + --$i; + } + ++$i; + if ($node->children[$i]->isFull($this->minDegree)) { + $this->splitChild($node, $i, $node->children[$i]); + if ($key > $node->keys[$i]) { + ++$i; + } + } + $this->insertNonFull($node->children[$i], $key); + } + } + + private function splitChild(BPlusTreeNode $parent, int $index, BPlusTreeNode $fullNode): void + { + $newNode = new BPlusTreeNode($this->minDegree); + $newNode->isLeaf = $fullNode->isLeaf; + $newNode->keyCount = $this->minDegree - 1; + + for ($j = 0; $j < $this->minDegree - 1; ++$j) { + $newNode->keys[$j] = $fullNode->keys[$j + $this->minDegree]; + } + + if (! $fullNode->isLeaf) { + for ($j = 0; $j < $this->minDegree; ++$j) { + $newNode->children[$j] = $fullNode->children[$j + $this->minDegree]; + } + } + + $fullNode->keyCount = $this->minDegree - 1; + + for ($j = $parent->keyCount; $j >= $index + 1; --$j) { + $parent->children[$j + 1] = $parent->children[$j]; + } + + $parent->children[$index + 1] = $newNode; + + for ($j = $parent->keyCount - 1; $j >= $index; --$j) { + $parent->keys[$j + 1] = $parent->keys[$j]; + } + + $parent->keys[$index] = $fullNode->keys[$this->minDegree - 1]; + ++$parent->keyCount; + } + + public function remove(mixed $key): void + { + if (! $this->root) { + return; + } + + $this->removeFromNode($this->root, $key); + + if (0 === $this->root->keyCount) { + $this->root = $this->root->isLeaf() ? null : $this->root->children[0]; + } + } + + private function removeFromNode(BPlusTreeNode $node, mixed $key): void + { + $idx = $this->findKey($node, $key); + + if ($this->keyExistsInNode($node, $idx, $key)) { + $node->isLeaf() ? $this->removeFromLeaf($node, $idx) : $this->removeFromNonLeaf($node, $idx); + } else { + if ($node->isLeaf()) { + return; + } + + $this->handleChildUnderflow($node, $idx, $key); + } + } + + private function keyExistsInNode(BPlusTreeNode $node, int $idx, mixed $key): bool + { + return $idx < $node->keyCount && $node->keys[$idx] === $key; + } + + private function handleChildUnderflow(BPlusTreeNode $node, int $idx, mixed $key): void + { + $flag = ($idx === $node->keyCount); + + if ($node->children[$idx]->isUnderflow($this->minDegree)) { + $this->fill($node, $idx); + } + + $this->removeFromNode( + $node->children[$flag && $idx > $node->keyCount ? $idx - 1 : $idx], + $key + ); + } + + private function removeFromLeaf(BPlusTreeNode $node, int $index): void + { + for ($i = $index + 1; $i < $node->keyCount; ++$i) { + $node->keys[$i - 1] = $node->keys[$i]; + } + --$node->keyCount; + } + + private function removeFromNonLeaf(BPlusTreeNode $node, int $index): void + { + $key = $node->keys[$index]; + + if ($node->children[$index]->keyCount >= $this->minDegree) { + $pred = $this->getPredecessor($node, $index); + $node->keys[$index] = $pred; + $this->removeFromNode($node->children[$index], $pred); + } elseif ($node->children[$index + 1]->keyCount >= $this->minDegree) { + $succ = $this->getSuccessor($node, $index); + $node->keys[$index] = $succ; + $this->removeFromNode($node->children[$index + 1], $succ); + } else { + $this->merge($node, $index); + $this->removeFromNode($node->children[$index], $key); + } + } + + private function getPredecessor(BPlusTreeNode $node, int $index): mixed + { + $current = $node->children[$index]; + while (! $current->isLeaf) { + $current = $current->children[$current->keyCount]; + } + + return $current->keys[$current->keyCount - 1]; + } + + private function getSuccessor(BPlusTreeNode $node, int $index): mixed + { + $current = $node->children[$index + 1]; + while (! $current->isLeaf) { + $current = $current->children[0]; + } + + return $current->keys[0]; + } + + private function fill(BPlusTreeNode $node, int $index): void + { + if (0 !== $index && $node->children[$index - 1]->keyCount >= $this->minDegree) { + $this->borrowFromPrevious($node, $index); + } elseif ($index !== $node->keyCount && $node->children[$index + 1]->keyCount >= $this->minDegree) { + $this->borrowFromNext($node, $index); + } else { + $this->merge($node, $index !== $node->keyCount ? $index : $index - 1); + } + } + + private function borrowFromPrevious(BPlusTreeNode $node, int $index): void + { + $child = $node->children[$index]; + $sibling = $node->children[$index - 1]; + + for ($i = $child->keyCount - 1; $i >= 0; --$i) { + $child->keys[$i + 1] = $child->keys[$i]; + } + + if (! $child->isLeaf) { + for ($i = $child->keyCount; $i >= 0; --$i) { + $child->children[$i + 1] = $child->children[$i]; + } + } + + $child->keys[0] = $node->keys[$index - 1]; + + if (! $child->isLeaf) { + $child->children[0] = $sibling->children[$sibling->keyCount]; + } + + $node->keys[$index - 1] = $sibling->keys[$sibling->keyCount - 1]; + + ++$child->keyCount; + --$sibling->keyCount; + } + + private function borrowFromNext(BPlusTreeNode $node, int $index): void + { + $child = $node->children[$index]; + $sibling = $node->children[$index + 1]; + + $child->keys[$child->keyCount] = $node->keys[$index]; + + if (! $child->isLeaf) { + $child->children[$child->keyCount + 1] = $sibling->children[0]; + } + + $node->keys[$index] = $sibling->keys[0]; + + for ($i = 1; $i < $sibling->keyCount; ++$i) { + $sibling->keys[$i - 1] = $sibling->keys[$i]; + } + + if (! $sibling->isLeaf) { + for ($i = 1; $i <= $sibling->keyCount; ++$i) { + $sibling->children[$i - 1] = $sibling->children[$i]; + } + } + + ++$child->keyCount; + --$sibling->keyCount; + } + + private function merge(BPlusTreeNode $node, int $index): void + { + $child = $node->children[$index]; + $sibling = $node->children[$index + 1]; + + $child->keys[$this->minDegree - 1] = $node->keys[$index]; + + for ($i = 0; $i < $sibling->keyCount; ++$i) { + $child->keys[$i + $this->minDegree] = $sibling->keys[$i]; + } + + if (! $child->isLeaf) { + for ($i = 0; $i <= $sibling->keyCount; ++$i) { + $child->children[$i + $this->minDegree] = $sibling->children[$i]; + } + } + + for ($i = $index + 1; $i < $node->keyCount; ++$i) { + $node->keys[$i - 1] = $node->keys[$i]; + } + + for ($i = $index + 2; $i <= $node->keyCount; ++$i) { + $node->children[$i - 1] = $node->children[$i]; + } + + $child->keyCount += $sibling->keyCount + 1; + --$node->keyCount; + } + + private function findKey(BPlusTreeNode $node, mixed $key): int + { + $index = 0; + while ($index < $node->keyCount && $node->keys[$index] < $key) { + ++$index; + } + + return $index; + } + + public function search(mixed $key): ?BPlusTreeNode + { + return $this->searchKeyInNode($this->root, $key); + } + + private function searchKeyInNode(?BPlusTreeNode $node, mixed $key): ?BPlusTreeNode + { + if (null === $node) { + return null; + } + + $i = 0; + while ($i < $node->keyCount && $key > $node->keys[$i]) { + ++$i; + } + if ($i < $node->keyCount && $key == $node->keys[$i]) { + return $node; + } + if ($node->isLeaf()) { + return null; + } + + return $this->searchKeyInNode($node->children[$i], $key); + } + + public function printTree(): void + { + if ($this->root) { + $this->printNode($this->root, 0); + } else { + echo "The tree is empty.\n"; + } + } + + private function printNode(BPlusTreeNode $node, int $level): void + { + echo str_repeat(' ', $level); + echo "Level $level: "; + for ($i = 0; $i < $node->keyCount; ++$i) { + echo $node->keys[$i] . ' '; + } + echo "\n"; + + if (! $node->isLeaf()) { + for ($i = 0; $i <= $node->keyCount; ++$i) { + $this->printNode($node->children[$i], $level + 1); + } + } + } +} + +class BPlusTreeSearchOptimizations +{ + /** + * Busca binária adaptada para BPlusTree. + */ + public static function binarySearch(BPlusTree $tree, mixed $key): ?BPlusTreeNode + { + $node = $tree->getRoot(); + while (null !== $node) { + $index = self::binarySearchInNode($node, $key); + if ($index < $node->keyCount && $node->keys[$index] === $key) { + return $node; + } + if ($node->isLeaf()) { + return null; + } + $node = $node->children[$index]; + } + + return null; + } + + private static function binarySearchInNode(BPlusTreeNode $node, mixed $key): int + { + $left = 0; + $right = $node->keyCount - 1; + + while ($left <= $right) { + $mid = $left + (($right - $left) >> 1); + + if ($node->keys[$mid] === $key) { + return $mid; + } + + if ($node->keys[$mid] < $key) { + $left = $mid + 1; + } else { + $right = $mid - 1; + } + } + + return $left; + } + + /** + * Busca por interpolação adaptada para BPlusTree. + */ + public static function interpolationSearch(BPlusTree $tree, int|float $key): ?BPlusTreeNode + { + $node = $tree->getRoot(); + while (null !== $node) { + $index = self::interpolationSearchInNode($node, $key); + if ($index < $node->keyCount && $node->keys[$index] === $key) { + return $node; + } + if ($node->isLeaf()) { + return null; + } + $node = $node->children[$index]; + } + + return null; + } + + private static function interpolationSearchInNode(BPlusTreeNode $node, int|float $key): int + { + $low = 0; + $high = $node->keyCount - 1; + + while ($low <= $high && $key >= $node->keys[$low] && $key <= $node->keys[$high]) { + if ($low === $high) { + return $low; + } + + $pos = $low + (($high - $low) / ($node->keys[$high] - $node->keys[$low])) * ($key - $node->keys[$low]); + $pos = (int) $pos; + + if ($node->keys[$pos] === $key) { + return $pos; + } + + if ($node->keys[$pos] < $key) { + $low = $pos + 1; + } else { + $high = $pos - 1; + } + } + + return $low; + } + + /** + * Busca exponencial adaptada para BPlusTree. + */ + public static function exponentialSearch(BPlusTree $tree, mixed $key): ?BPlusTreeNode + { + $node = $tree->getRoot(); + while (null !== $node) { + $index = self::exponentialSearchInNode($node, $key); + if ($index < $node->keyCount && $node->keys[$index] === $key) { + return $node; + } + if ($node->isLeaf()) { + return null; + } + $node = $node->children[$index]; + } + + return null; + } + + private static function exponentialSearchInNode(BPlusTreeNode $node, mixed $key): int + { + if (0 === $node->keyCount) { + return 0; + } + + $bound = 1; + while ($bound < $node->keyCount && $node->keys[$bound - 1] < $key) { + $bound *= 2; + } + + return self::binarySearchBounded($node, $key, (int) ($bound / 2), min($bound, $node->keyCount)); + } + + private static function binarySearchBounded(BPlusTreeNode $node, mixed $key, int $left, int $right): int + { + while ($left <= $right) { + $mid = $left + (($right - $left) >> 1); + + if ($node->keys[$mid] === $key) { + return $mid; + } + + if ($node->keys[$mid] < $key) { + $left = $mid + 1; + } else { + $right = $mid - 1; + } + } + + return $left; + } +} + +// Exemplo de uso +$tree = new BPlusTree(3); // Criando uma árvore B+ com grau mínimo 3 +for ($i = 1; $i <= 1000000; ++$i) { + $tree->insert($i); +} + +$target = 500000; + +$start = microtime(true); +$result = BPlusTreeSearchOptimizations::binarySearch($tree, $target); +$end = microtime(true); +echo 'Busca Binária: ' . ($end - $start) . " segundos\n"; + +$start = microtime(true); +$result = BPlusTreeSearchOptimizations::interpolationSearch($tree, $target); +$end = microtime(true); +echo 'Busca por Interpolação: ' . ($end - $start) . " segundos\n"; + +$start = microtime(true); +$result = BPlusTreeSearchOptimizations::exponentialSearch($tree, $target); +$end = microtime(true); +echo 'Busca Exponencial: ' . ($end - $start) . " segundos\n"; From 25e0a07e039f76197a5bde7723e5b6ace5df18c9 Mon Sep 17 00:00:00 2001 From: Walmir Silva Date: Tue, 24 Sep 2024 11:31:16 -0300 Subject: [PATCH 3/4] Refactor B+ Tree code for improved readability and maintainability - Restructured methods in `BPlusTree`, `BPlusTreeInternalNode`, and `BPlusTreeLeafNode` classes to enhance code clarity and organization. - Removed redundant code and simplified logical conditions for better performance. - Standardized variable and method naming conventions for consistency throughout the codebase. - Updated tests in `BPlusTreeTest.php` to reflect the changes made to the classes and ensure functional integrity. - Prepared the code for future enhancements and extensions in the B+ Tree implementation. - Added `BPlusTreeSearcherTest.php` (untracked file) for additional testing of search functionality.' --- src/Tree/BPlusTree.php | 45 ++-- .../BPlusTreeNode/BPlusTreeInternalNode.php | 15 +- src/Tree/BPlusTreeNode/BPlusTreeLeafNode.php | 8 +- tests/Tree/BPlusTreeSearcherTest.php | 221 ++++++++++++++++++ tests/Tree/BPlusTreeTest.php | 144 ++++++++++-- 5 files changed, 386 insertions(+), 47 deletions(-) create mode 100644 tests/Tree/BPlusTreeSearcherTest.php diff --git a/src/Tree/BPlusTree.php b/src/Tree/BPlusTree.php index e979ed2..79c971c 100644 --- a/src/Tree/BPlusTree.php +++ b/src/Tree/BPlusTree.php @@ -47,9 +47,9 @@ public function __construct(private int $order) if ($order < 3) { throw new \InvalidArgumentException('Order must be at least 3'); } - $this->searcher = new BPlusTreeSearcher(); $this->order = $order; - $this->root = new BPlusTreeLeafNode($order); + $this->searcher = new BPlusTreeSearcher(); + $this->root = null; } public function getRoot(): ?BPlusTreeNode @@ -64,6 +64,10 @@ public function add(mixed $element): void public function insert(int $key, mixed $value): void { + if (null === $this->root) { + $this->root = new BPlusTreeLeafNode($this->order); + } + $newRoot = $this->root->insert($key, $value); if ($newRoot !== $this->root) { $this->root = $newRoot; @@ -83,6 +87,8 @@ public function remove(mixed $element): bool --$this->size; if ($this->root instanceof BPlusTreeInternalNode && 0 === count($this->root->keys)) { $this->root = $this->root->children[0]; + } elseif ($this->root instanceof BPlusTreeLeafNode && 0 === count($this->root->keys)) { + $this->root = null; // Set root to null when tree is empty } } @@ -122,6 +128,10 @@ public function get(int $key): mixed public function set(int $key, mixed $value): void { + if (null === $this->root) { + throw new \OutOfRangeException('Key not found'); + } + $node = $this->root; while ($node instanceof BPlusTreeInternalNode) { $index = 0; @@ -131,6 +141,10 @@ public function set(int $key, mixed $value): void $node = $node->children[$index]; } + if (null === $node) { + throw new \OutOfRangeException('Key not found'); + } + /** @var BPlusTreeLeafNode $node */ $index = array_search($key, $node->keys); if (false !== $index) { @@ -196,30 +210,35 @@ public function isBalanced(): bool return true; } - return false !== $this->checkBalance($this->root); + $leafDepth = null; + + return $this->checkBalance($this->root, 0, $leafDepth); } - private function checkBalance(?BPlusTreeNode $node): int + private function checkBalance(?BPlusTreeNode $node, int $currentDepth = 0, ?int &$leafDepth = null): bool { if (null === $node) { - return 0; + return true; } if ($node instanceof BPlusTreeLeafNode) { - return 1; + if (null === $leafDepth) { + $leafDepth = $currentDepth; + } elseif ($currentDepth !== $leafDepth) { + return false; + } + + return true; } /** @var BPlusTreeInternalNode $node */ - $height = $this->checkBalance($node->children[0]); - - for ($i = 1; $i < count($node->children); ++$i) { - $childHeight = $this->checkBalance($node->children[$i]); - if ($childHeight !== $height) { - throw new \RuntimeException('B+ Tree is not balanced'); + foreach ($node->children as $child) { + if (! $this->checkBalance($child, $currentDepth + 1, $leafDepth)) { + return false; } } - return $height + 1; + return true; } public function sort(): void diff --git a/src/Tree/BPlusTreeNode/BPlusTreeInternalNode.php b/src/Tree/BPlusTreeNode/BPlusTreeInternalNode.php index edf8924..6af5add 100644 --- a/src/Tree/BPlusTreeNode/BPlusTreeInternalNode.php +++ b/src/Tree/BPlusTreeNode/BPlusTreeInternalNode.php @@ -51,30 +51,27 @@ private function findIndex(int $key): int private function split(): BPlusTreeInternalNode { - $order = $this->order; - $middleKeyIndex = (int) (($order - 1) / 2); + $numKeys = count($this->keys); + $middleKeyIndex = intdiv($numKeys, 2); $middleKey = $this->keys[$middleKeyIndex]; - // Create left and right nodes - $leftNode = new BPlusTreeInternalNode($order); - $rightNode = new BPlusTreeInternalNode($order); + $leftNode = new BPlusTreeInternalNode($this->order); + $rightNode = new BPlusTreeInternalNode($this->order); - // Left node keys and children $leftNode->keys = array_slice($this->keys, 0, $middleKeyIndex); $leftNode->children = array_slice($this->children, 0, $middleKeyIndex + 1); - // Right node keys and children $rightNode->keys = array_slice($this->keys, $middleKeyIndex + 1); $rightNode->children = array_slice($this->children, $middleKeyIndex + 1); - // Create a new parent node and promote the middle key - $parent = new BPlusTreeInternalNode($order); + $parent = new BPlusTreeInternalNode($this->order); $parent->keys = [$middleKey]; $parent->children = [$leftNode, $rightNode]; return $parent; } + public function search(mixed $key): mixed { $index = $this->findInsertionIndex($key); diff --git a/src/Tree/BPlusTreeNode/BPlusTreeLeafNode.php b/src/Tree/BPlusTreeNode/BPlusTreeLeafNode.php index 6025de2..5036548 100644 --- a/src/Tree/BPlusTreeNode/BPlusTreeLeafNode.php +++ b/src/Tree/BPlusTreeNode/BPlusTreeLeafNode.php @@ -41,9 +41,9 @@ private function findIndex(int $key): int private function split(): BPlusTreeInternalNode { - $order = $this->order; - $middleIndex = (int) ($order / 2); - $newNode = new BPlusTreeLeafNode($order); + $numKeys = count($this->keys); + $middleIndex = intdiv($numKeys, 2); + $newNode = new BPlusTreeLeafNode($this->order); $newNode->keys = array_slice($this->keys, $middleIndex); $newNode->values = array_slice($this->values, $middleIndex); @@ -54,7 +54,7 @@ private function split(): BPlusTreeInternalNode $newNode->next = $this->next; $this->next = $newNode; - $parent = new BPlusTreeInternalNode($order); + $parent = new BPlusTreeInternalNode($this->order); $parent->keys = [$newNode->keys[0]]; $parent->children = [$this, $newNode]; diff --git a/tests/Tree/BPlusTreeSearcherTest.php b/tests/Tree/BPlusTreeSearcherTest.php new file mode 100644 index 0000000..46b3ba6 --- /dev/null +++ b/tests/Tree/BPlusTreeSearcherTest.php @@ -0,0 +1,221 @@ +tree = new BPlusTree(4); // Using order 4 for the tree + $this->searcher = new BPlusTreeSearcher(); + } + + public function testFindByKey(): void + { + // Insert elements into the tree + $this->tree->insert(10, 'Value10'); + $this->tree->insert(20, 'Value20'); + $this->tree->insert(30, 'Value30'); + $this->tree->insert(40, 'Value40'); + + // Test finding existing keys + $result = $this->searcher->find($this->tree, 20); + $this->assertEquals('Value20', $result); + + $result = $this->searcher->find($this->tree, 40); + $this->assertEquals('Value40', $result); + + // Test finding a non-existing key + $result = $this->searcher->find($this->tree, 50); + $this->assertNull($result); + } + + public function testFindByValue(): void + { + // Insert elements into the tree + $this->tree->insert(5, 'Value5'); + $this->tree->insert(15, 'Value15'); + $this->tree->insert(25, 'Value25'); + $this->tree->insert(35, 'Value35'); + + // Test finding existing values + $result = $this->searcher->find($this->tree, 'Value15'); + $this->assertEquals(15, $result); + + $result = $this->searcher->find($this->tree, 'Value35'); + $this->assertEquals(35, $result); + + // Test finding a non-existing value + $result = $this->searcher->find($this->tree, 'Value50'); + $this->assertNull($result); + } + + public function testRangeSearch(): void + { + // Insert elements into the tree + for ($i = 1; $i <= 20; ++$i) { + $this->tree->insert($i * 5, 'Value' . ($i * 5)); + } + + // Perform a range search + $result = $this->searcher->rangeSearch($this->tree, 30, 70); + + // Expected values are from 30 to 70 (inclusive) + $expected = ['Value30', 'Value35', 'Value40', 'Value45', 'Value50', 'Value55', 'Value60', 'Value65', 'Value70']; + + $this->assertEquals($expected, $result); + + // Test an empty range + $result = $this->searcher->rangeSearch($this->tree, 105, 110); + $this->assertEmpty($result); + } + + public function testSearchInLeafNode(): void + { + // Directly test the private method searchInLeafNode using reflection + $leafNode = new BPlusTreeLeafNode(4); + $leafNode->keys = [10, 20, 30]; + $leafNode->values = ['Value10', 'Value20', 'Value30']; + + // Use reflection to access the private method + $reflection = new ReflectionClass(BPlusTreeSearcher::class); + $method = $reflection->getMethod('searchInLeafNode'); + $method->setAccessible(true); + + // Test finding an existing value + $index = $method->invokeArgs($this->searcher, [$leafNode, 'Value20']); + $this->assertEquals(20, $index); + + // Test finding a non-existing value + $index = $method->invokeArgs($this->searcher, [$leafNode, 'Value50']); + $this->assertNull($index); + } + + public function testCompareValues(): void + { + // Directly test the private method compareValues using reflection + $reflection = new ReflectionClass(BPlusTreeSearcher::class); + $method = $reflection->getMethod('compareValues'); + $method->setAccessible(true); + + // Test comparison of scalars + $result = $method->invokeArgs($this->searcher, [10, 10]); + $this->assertTrue($result); + + $result = $method->invokeArgs($this->searcher, [10, 20]); + $this->assertFalse($result); + + // Test comparison of objects + $obj1 = (object) ['a' => 1]; + $obj2 = (object) ['a' => 1]; + $obj3 = (object) ['a' => 2]; + + $result = $method->invokeArgs($this->searcher, [$obj1, $obj2]); + $this->assertTrue($result); + + $result = $method->invokeArgs($this->searcher, [$obj1, $obj3]); + $this->assertFalse($result); + } + + public function testFindFirstLeafNode(): void + { + // Create a tree with multiple levels + for ($i = 1; $i <= 50; ++$i) { + $this->tree->insert($i, "Value$i"); + } + + // Use reflection to access the private method + $reflection = new ReflectionClass(BPlusTreeSearcher::class); + $method = $reflection->getMethod('findFirstLeafNode'); + $method->setAccessible(true); + + $firstLeaf = $method->invokeArgs($this->searcher, [$this->tree->getRoot()]); + $this->assertInstanceOf(BPlusTreeLeafNode::class, $firstLeaf); + + // The first leaf node should contain the smallest keys + $this->assertContains(1, $firstLeaf->keys); + } + + public function testSearchByValueWithNonExistingValue(): void + { + // Insert elements into the tree + $this->tree->insert(100, 'Value100'); + $this->tree->insert(200, 'Value200'); + + // Test searching for a non-existing value + $result = $this->searcher->find($this->tree, 'NonExistingValue'); + $this->assertNull($result); + } + + public function testSearchWithEmptyTree(): void + { + // Create an empty tree + $emptyTree = new BPlusTree(4); + + // Test searching in an empty tree + $result = $this->searcher->find($emptyTree, 10); + $this->assertNull($result); + + $result = $this->searcher->rangeSearch($emptyTree, 10, 20); + $this->assertEmpty($result); + } + + public function testFindWithNullValue(): void + { + // Insert elements with null values + $this->tree->insert(1, null); + $this->tree->insert(2, 'Value2'); + + // Test finding a key with a null value + $result = $this->searcher->find($this->tree, null); + $this->assertEquals(1, $result); + } + + public function testSearchWithDuplicateValues(): void + { + // Insert elements with duplicate values + $this->tree->insert(1, 'DuplicateValue'); + $this->tree->insert(2, 'DuplicateValue'); + $this->tree->insert(3, 'UniqueValue'); + + // Test finding by value (should return the first key that matches) + $result = $this->searcher->find($this->tree, 'DuplicateValue'); + $this->assertEquals(1, $result); + + // Test that the search continues correctly after duplicates + $result = $this->searcher->find($this->tree, 'UniqueValue'); + $this->assertEquals(3, $result); + } + + public function testRangeSearchWithSingleElementRange(): void + { + // Insert elements into the tree + $this->tree->insert(10, 'Value10'); + $this->tree->insert(20, 'Value20'); + + // Perform a range search where start and end are the same + $result = $this->searcher->rangeSearch($this->tree, 20, 20); + $this->assertEquals(['Value20'], $result); + } + + public function testFindWithEmptyTree(): void + { + // Create an empty tree + $emptyTree = new BPlusTree(4); + + // Ensure the root is null + $this->assertNull($emptyTree->getRoot()); + + // Test finding an element in an empty tree + $result = $this->searcher->find($emptyTree, 10); + $this->assertNull($result); + } +} diff --git a/tests/Tree/BPlusTreeTest.php b/tests/Tree/BPlusTreeTest.php index f1f661f..ee71477 100644 --- a/tests/Tree/BPlusTreeTest.php +++ b/tests/Tree/BPlusTreeTest.php @@ -2,12 +2,14 @@ declare(strict_types=1); +use KaririCode\Contract\DataStructure\Structural\Collection; use KaririCode\DataStructure\Tree\BPlusTree; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class BPlusTreeTest extends TestCase { - public const ORDER = 5; + public const ORDER = 4; private BPlusTree $tree; @@ -39,7 +41,7 @@ public function testInsertAndRemove(): void $this->assertTrue($this->tree->remove(1)); $this->assertNull($this->tree->find(1)); - $this->assertFalse($this->tree->remove(5)); // Testa remoção de chave inexistente + $this->assertFalse($this->tree->remove(5)); // Test removing non-existent key } public function testRangeSearch(): void @@ -88,21 +90,7 @@ public function testGetMinimumAndMaximum(): void $this->assertEquals('E', $this->tree->getMaximum()); } - // public function testTreeBalance(): void - // { - // $this->tree->insert(1, "A"); - // $this->tree->insert(2, "B"); - // $this->tree->insert(3, "C"); - // $this->tree->insert(4, "D"); - // $this->tree->insert(5, "E"); - // $this->tree->insert(6, "F"); - // $this->tree->insert(7, "G"); - // $this->tree->insert(8, "H"); - - // $this->assertTrue($this->tree->isBalanced(), "B+ Tree is not balanced after insertions"); - // } - - public function testSetAndGetByIndex(): void + public function testSetAndGetByKey(): void { $this->tree->insert(1, 'A'); $this->tree->insert(2, 'B'); @@ -125,12 +113,126 @@ public function testVisualTreeStructure(): void $this->assertStringContainsString('Internal Node', $visual); } - public function testExceptions(): void + public function testConstructorWithInvalidOrder(): void + { + $this->expectException(InvalidArgumentException::class); + new BPlusTree(2); // Order less than 3 should throw exception + } + + public function testGetWithInvalidIndex(): void { $this->expectException(OutOfRangeException::class); - $this->tree->get(99); // Tenta acessar um índice fora do intervalo + $this->tree->get(99); // Attempt to access an invalid key + } - $this->expectException(InvalidArgumentException::class); - new BPlusTree(2); // Ordem menor que 3 deve lançar exceção + public function testAdd(): void + { + $this->tree->add(1); + $this->assertEquals(1, $this->tree->find(1)); + + $this->tree->add(2); + $this->assertEquals(2, $this->tree->find(2)); + + $this->assertEquals(2, $this->tree->size()); + } + + public function testRemoveUntilEmpty(): void + { + $this->tree->insert(1, 'A'); + $this->tree->insert(2, 'B'); + + $this->assertTrue($this->tree->remove(1)); + $this->assertTrue($this->tree->remove(2)); + + $this->assertNull($this->tree->getRoot()); + $this->assertEquals(0, $this->tree->size()); + } + + public function testContains(): void + { + $this->tree->insert(1, 'A'); + $this->assertTrue($this->tree->contains(1)); + $this->assertFalse($this->tree->contains(2)); + } + + public function testSetWithInvalidKey(): void + { + $this->expectException(OutOfRangeException::class); + $this->tree->set(99, 'Z'); // Key 99 does not exist + } + + public function testAddAll(): void + { + /** @var Collection|MockObject */ + $mockCollection = $this->createMock(Collection::class); + $mockCollection->method('getItems')->willReturn([1, 2, 3]); + + $this->tree->addAll($mockCollection); + + $this->assertEquals(3, $this->tree->size()); + $this->assertEquals(1, $this->tree->find(1)); + $this->assertEquals(2, $this->tree->find(2)); + $this->assertEquals(3, $this->tree->find(3)); + } + + public function testIsBalanced(): void + { + for ($i = 1; $i <= 10; ++$i) { + $this->tree->insert($i, "Value$i"); + } + + $this->assertTrue($this->tree->isBalanced(), 'Tree should be balanced after insertions'); + } + + public function testSort(): void + { + $this->tree->insert(3, 'C'); + $this->tree->insert(1, 'A'); + $this->tree->insert(2, 'B'); + + // Calling sort to check if the tree is sorted + $this->tree->sort(); + + // If no exception is thrown, the tree is sorted + $this->assertTrue(true); + } + + public function testGetLeftmostLeafWithEmptyTree(): void + { + $reflection = new ReflectionClass($this->tree); + $method = $reflection->getMethod('getLeftmostLeaf'); + $method->setAccessible(true); + + $result = $method->invoke($this->tree); + $this->assertNull($result); + } + + public function testGetRightmostLeafWithEmptyTree(): void + { + $reflection = new ReflectionClass($this->tree); + $method = $reflection->getMethod('getRightmostLeaf'); + $method->setAccessible(true); + + $result = $method->invoke($this->tree); + $this->assertNull($result); + } + + public function testVisualTreeStructureWithEmptyTree(): void + { + $visual = $this->tree->visualTreeStructure(); + $this->assertEquals('Empty tree', $visual); + } + + public function testRemoveNonExistentKey(): void + { + $this->tree->insert(1, 'A'); + $this->assertFalse($this->tree->remove(99)); + } + + public function testSetExistingKey(): void + { + $this->tree->insert(1, 'A'); + $this->tree->set(1, 'Z'); + $this->assertEquals('Z', $this->tree->get(1)); } } From 595db62f2723e818f501dad1f2036f39d78fcfcb Mon Sep 17 00:00:00 2001 From: Walmir Silva Date: Thu, 26 Sep 2024 15:43:29 -0300 Subject: [PATCH 4/4] feat: remove B+ Tree implementation due to logical errors The current implementation of the B+ Tree contains critical logical issues, particularly in tree balancing and search operations. All related classes and tests have been removed to prevent further issues. The algorithm will be rewritten in the future with a more robust approach. Removed files include: --- src/Tree/BPlusTree.php | 319 ------------------ .../BPlusTreeNode/BPlusTreeInternalNode.php | 105 ------ src/Tree/BPlusTreeNode/BPlusTreeLeafNode.php | 103 ------ src/Tree/BPlusTreeNode/BPlusTreeNode.php | 38 --- src/Tree/BPlusTreeSearcher.php | 135 -------- .../BPlusTreeInternalNodeTest.php | 74 ---- .../BPlusTreeNode/BPlusTreeLeafNodeTest.php | 92 ----- tests/Tree/BPlusTreeSearcherTest.php | 221 ------------ tests/Tree/BPlusTreeTest.php | 238 ------------- 9 files changed, 1325 deletions(-) delete mode 100644 src/Tree/BPlusTree.php delete mode 100644 src/Tree/BPlusTreeNode/BPlusTreeInternalNode.php delete mode 100644 src/Tree/BPlusTreeNode/BPlusTreeLeafNode.php delete mode 100644 src/Tree/BPlusTreeNode/BPlusTreeNode.php delete mode 100644 src/Tree/BPlusTreeSearcher.php delete mode 100644 tests/Tree/BPlusTreeNode/BPlusTreeInternalNodeTest.php delete mode 100644 tests/Tree/BPlusTreeNode/BPlusTreeLeafNodeTest.php delete mode 100644 tests/Tree/BPlusTreeSearcherTest.php delete mode 100644 tests/Tree/BPlusTreeTest.php diff --git a/src/Tree/BPlusTree.php b/src/Tree/BPlusTree.php deleted file mode 100644 index 79c971c..0000000 --- a/src/Tree/BPlusTree.php +++ /dev/null @@ -1,319 +0,0 @@ - - * @license MIT - * - * @see https://kariricode.org/ - */ -class BPlusTree implements BPlusTreeCollection -{ - private ?BPlusTreeNode $root = null; - private BPlusTreeSearcher $searcher; - private int $size = 0; - - public function __construct(private int $order) - { - if ($order < 3) { - throw new \InvalidArgumentException('Order must be at least 3'); - } - $this->order = $order; - $this->searcher = new BPlusTreeSearcher(); - $this->root = null; - } - - public function getRoot(): ?BPlusTreeNode - { - return $this->root; - } - - public function add(mixed $element): void - { - $this->insert($element, $element); - } - - public function insert(int $key, mixed $value): void - { - if (null === $this->root) { - $this->root = new BPlusTreeLeafNode($this->order); - } - - $newRoot = $this->root->insert($key, $value); - if ($newRoot !== $this->root) { - $this->root = $newRoot; - } - - ++$this->size; - } - - public function remove(mixed $element): bool - { - if (null === $this->root) { - return false; - } - - $result = $this->root->remove($element); - if ($result) { - --$this->size; - if ($this->root instanceof BPlusTreeInternalNode && 0 === count($this->root->keys)) { - $this->root = $this->root->children[0]; - } elseif ($this->root instanceof BPlusTreeLeafNode && 0 === count($this->root->keys)) { - $this->root = null; // Set root to null when tree is empty - } - } - - return $result; - } - - public function clear(): void - { - $this->root = null; - $this->size = 0; - } - - public function find(mixed $element): mixed - { - return $this->searcher->find($this, $element); - } - - public function contains(mixed $element): bool - { - return null !== $this->find($element); - } - - public function rangeSearch(mixed $start, mixed $end): array - { - return $this->searcher->rangeSearch($this, $start, $end); - } - - public function get(int $key): mixed - { - $value = $this->find($key); - if (null !== $value) { - return $value; - } else { - throw new \OutOfRangeException('Key not found'); - } - } - - public function set(int $key, mixed $value): void - { - if (null === $this->root) { - throw new \OutOfRangeException('Key not found'); - } - - $node = $this->root; - while ($node instanceof BPlusTreeInternalNode) { - $index = 0; - while ($index < count($node->keys) && $key >= $node->keys[$index]) { - ++$index; - } - $node = $node->children[$index]; - } - - if (null === $node) { - throw new \OutOfRangeException('Key not found'); - } - - /** @var BPlusTreeLeafNode $node */ - $index = array_search($key, $node->keys); - if (false !== $index) { - $node->values[$index] = $value; - } else { - throw new \OutOfRangeException('Key not found'); - } - } - - public function size(): int - { - return $this->size; - } - - public function getItems(): array - { - $items = []; - $current = $this->getLeftmostLeaf(); - while (null !== $current) { - $items = array_merge($items, $current->values); - $current = $current->next; - } - - return $items; - } - - public function addAll(Collection $collection): void - { - foreach ($collection->getItems() as $item) { - $this->add($item); - } - } - - public function getOrder(): int - { - return $this->order; - } - - public function getMinimum(): mixed - { - $leftmostLeaf = $this->getLeftmostLeaf(); - - return null !== $leftmostLeaf ? $leftmostLeaf->values[0] : null; - } - - public function getMaximum(): mixed - { - $rightmostLeaf = $this->getRightmostLeaf(); - - return null !== $rightmostLeaf ? $rightmostLeaf->values[count($rightmostLeaf->values) - 1] : null; - } - - public function balance(): void - { - // A B+ Tree is self-balancing, so we don't need to implement any additional balancing logic. - // However, we can perform a check to ensure the tree is balanced. - $this->checkBalance($this->root); - } - - public function isBalanced(): bool - { - if (null === $this->root) { - return true; - } - - $leafDepth = null; - - return $this->checkBalance($this->root, 0, $leafDepth); - } - - private function checkBalance(?BPlusTreeNode $node, int $currentDepth = 0, ?int &$leafDepth = null): bool - { - if (null === $node) { - return true; - } - - if ($node instanceof BPlusTreeLeafNode) { - if (null === $leafDepth) { - $leafDepth = $currentDepth; - } elseif ($currentDepth !== $leafDepth) { - return false; - } - - return true; - } - - /** @var BPlusTreeInternalNode $node */ - foreach ($node->children as $child) { - if (! $this->checkBalance($child, $currentDepth + 1, $leafDepth)) { - return false; - } - } - - return true; - } - - public function sort(): void - { - // B+ Tree is always sorted, so this method doesn't need to do anything. - // However, we can perform a check to ensure the tree is sorted. - $this->checkSorted(); - } - - private function checkSorted(): void - { - $current = $this->getLeftmostLeaf(); - $prev = null; - - while (null !== $current) { - foreach ($current->values as $value) { - if (null !== $prev && $value < $prev) { - throw new \RuntimeException('B+ Tree is not sorted'); - } - $prev = $value; - } - $current = $current->next; - } - } - - private function getLeftmostLeaf(): ?BPlusTreeLeafNode - { - $current = $this->root; - while ($current instanceof BPlusTreeInternalNode) { - $current = $current->children[0]; - } - - return $current; - } - - private function getRightmostLeaf(): ?BPlusTreeLeafNode - { - $current = $this->root; - while ($current instanceof BPlusTreeInternalNode) { - $current = $current->children[count($current->children) - 1]; - } - - return $current; - } - - public function visualTreeStructure(): string - { - if (null === $this->root) { - return 'Empty tree'; - } - - return $this->visualizeNode($this->root); - } - - private function visualizeNode(BPlusTreeNode $node, int $depth = 0): string - { - $indent = str_repeat(' ', $depth); - $output = ''; - - if ($node instanceof BPlusTreeInternalNode) { - $output .= $indent . "Internal Node:\n"; - $output .= $indent . ' Keys: ' . implode(', ', $node->keys) . "\n"; - foreach ($node->children as $index => $child) { - $output .= $indent . ' Child ' . ($index + 1) . ":\n"; - $output .= $this->visualizeNode($child, $depth + 2); - } - } elseif ($node instanceof BPlusTreeLeafNode) { - $output .= $indent . "Leaf Node:\n"; - $output .= $indent . ' Keys: ' . implode(', ', $node->keys) . "\n"; - $output .= $indent . ' Values: ' . implode(', ', $node->values) . "\n"; - if ($node->next) { - $output .= $indent . ' Next Leaf -> [' . implode(', ', $node->next->keys) . "]\n"; - } - } - - return $output; - } -} diff --git a/src/Tree/BPlusTreeNode/BPlusTreeInternalNode.php b/src/Tree/BPlusTreeNode/BPlusTreeInternalNode.php deleted file mode 100644 index 6af5add..0000000 --- a/src/Tree/BPlusTreeNode/BPlusTreeInternalNode.php +++ /dev/null @@ -1,105 +0,0 @@ - - * @license MIT - * - * @see https://kariricode.org/ - */ -class BPlusTreeInternalNode extends BPlusTreeNode -{ - public array $keys = []; - public array $children = []; - - public function insert(int $key, $value): BPlusTreeNode - { - $index = $this->findIndex($key); - $child = $this->children[$index]; - $newChild = $child->insert($key, $value); - - if ($newChild !== $child) { - // Insert the new key and child into the current node - array_splice($this->keys, $index, 0, [$newChild->keys[0]]); - array_splice($this->children, $index + 1, 0, [$newChild]); - } - - if ($this->isFull()) { - return $this->split(); - } - - return $this; - } - - private function findIndex(int $key): int - { - $index = 0; - while ($index < count($this->keys) && $key >= $this->keys[$index]) { - ++$index; - } - - return $index; - } - - private function split(): BPlusTreeInternalNode - { - $numKeys = count($this->keys); - $middleKeyIndex = intdiv($numKeys, 2); - $middleKey = $this->keys[$middleKeyIndex]; - - $leftNode = new BPlusTreeInternalNode($this->order); - $rightNode = new BPlusTreeInternalNode($this->order); - - $leftNode->keys = array_slice($this->keys, 0, $middleKeyIndex); - $leftNode->children = array_slice($this->children, 0, $middleKeyIndex + 1); - - $rightNode->keys = array_slice($this->keys, $middleKeyIndex + 1); - $rightNode->children = array_slice($this->children, $middleKeyIndex + 1); - - $parent = new BPlusTreeInternalNode($this->order); - $parent->keys = [$middleKey]; - $parent->children = [$leftNode, $rightNode]; - - return $parent; - } - - - public function search(mixed $key): mixed - { - $index = $this->findInsertionIndex($key); - - return $this->children[$index]->search($key); - } - - public function remove(mixed $key): bool - { - $index = $this->findInsertionIndex($key); - - return $this->children[$index]->remove($key); - } - - private function findInsertionIndex(mixed $key): int - { - $left = 0; - $right = count($this->keys); - - while ($left < $right) { - $mid = ($left + $right) >> 1; - if ($this->keys[$mid] <= $key) { - $left = $mid + 1; - } else { - $right = $mid; - } - } - - return $left; - } -} diff --git a/src/Tree/BPlusTreeNode/BPlusTreeLeafNode.php b/src/Tree/BPlusTreeNode/BPlusTreeLeafNode.php deleted file mode 100644 index 5036548..0000000 --- a/src/Tree/BPlusTreeNode/BPlusTreeLeafNode.php +++ /dev/null @@ -1,103 +0,0 @@ -findIndex($key); - - array_splice($this->keys, $index, 0, [$key]); - array_splice($this->values, $index, 0, [$value]); - - if ($this->isFull()) { - return $this->split(); - } - - return $this; - } - - private function findIndex(int $key): int - { - $index = 0; - while ($index < count($this->keys) && $key > $this->keys[$index]) { - ++$index; - } - - return $index; - } - - private function split(): BPlusTreeInternalNode - { - $numKeys = count($this->keys); - $middleIndex = intdiv($numKeys, 2); - $newNode = new BPlusTreeLeafNode($this->order); - - $newNode->keys = array_slice($this->keys, $middleIndex); - $newNode->values = array_slice($this->values, $middleIndex); - - $this->keys = array_slice($this->keys, 0, $middleIndex); - $this->values = array_slice($this->values, 0, $middleIndex); - - $newNode->next = $this->next; - $this->next = $newNode; - - $parent = new BPlusTreeInternalNode($this->order); - $parent->keys = [$newNode->keys[0]]; - $parent->children = [$this, $newNode]; - - return $parent; - } - - public function remove(mixed $key): bool - { - $index = $this->findInsertionIndex($key); - if ($index < count($this->keys) && $this->keys[$index] === $key) { - array_splice($this->keys, $index, 1); - array_splice($this->values, $index, 1); - - return true; - } - - return false; - } - - public function search(mixed $key): mixed - { - $index = $this->findInsertionIndex($key); - if ($index < count($this->keys) && $this->keys[$index] === $key) { - return $this->values[$index]; - } - - return null; - } - - private function findInsertionIndex(mixed $key): int - { - $left = 0; - $right = count($this->keys); - - while ($left < $right) { - $mid = ($left + $right) >> 1; - if ($this->keys[$mid] < $key) { - $left = $mid + 1; - } else { - $right = $mid; - } - } - - return $left; - } -} diff --git a/src/Tree/BPlusTreeNode/BPlusTreeNode.php b/src/Tree/BPlusTreeNode/BPlusTreeNode.php deleted file mode 100644 index 22093d9..0000000 --- a/src/Tree/BPlusTreeNode/BPlusTreeNode.php +++ /dev/null @@ -1,38 +0,0 @@ - - * @license MIT - * - * @see https://kariricode.org/ - */ -abstract class BPlusTreeNode -{ - public array $keys = []; - protected int $order; - - public function __construct(int $order) - { - $this->order = $order; - } - - public function isFull(): bool - { - return count($this->keys) >= $this->order - 1; - } - - abstract public function insert(int $key, mixed $value): BPlusTreeNode; - - abstract public function remove(mixed $key): bool; - - abstract public function search(mixed $input): mixed; -} diff --git a/src/Tree/BPlusTreeSearcher.php b/src/Tree/BPlusTreeSearcher.php deleted file mode 100644 index f3526b0..0000000 --- a/src/Tree/BPlusTreeSearcher.php +++ /dev/null @@ -1,135 +0,0 @@ - - * @license MIT - * - * @see https://kariricode.org/ - */ -class BPlusTreeSearcher -{ - public function find(BPlusTree $tree, mixed $element): mixed - { - $root = $tree->getRoot(); - if (null === $root) { - return null; - } - - return is_int($element) ? - $this->search($root, $element) : - $this->searchByValue($root, $element); - } - - private function search(BPlusTreeNode $node, int $key): mixed - { - if ($node instanceof BPlusTreeLeafNode) { - $index = array_search($key, $node->keys, true); - - return false !== $index ? $node->values[$index] : null; - } - - /** @var BPlusTreeInternalNode $node */ - $index = 0; - while ($index < count($node->keys) && $key >= $node->keys[$index]) { - ++$index; - } - - return $this->search($node->children[$index], $key); - } - - private function searchByValue(BPlusTreeNode $root, mixed $value): ?int - { - $leafNode = $this->findFirstLeafNode($root); - while (null !== $leafNode) { - $result = $this->searchInLeafNode($leafNode, $value); - if (null !== $result) { - return $result; - } - $leafNode = $leafNode->next; - } - - return null; - } - - private function findFirstLeafNode(BPlusTreeNode $node): ?BPlusTreeLeafNode - { - $current = $node; - while ($current instanceof BPlusTreeInternalNode) { - $current = $current->children[0]; - } - - return $current; - } - - private function searchInLeafNode(BPlusTreeLeafNode $node, mixed $value): ?int - { - $values = $node->values; - $keys = $node->keys; - foreach ($values as $index => $nodeValue) { - if ($this->compareValues($nodeValue, $value)) { - return $keys[$index]; - } - } - - return null; - } - - private function compareValues(mixed $a, mixed $b): bool - { - if (is_object($a) && is_object($b)) { - return $a == $b; // Use loose comparison for objects - } - - return $a === $b; // Use strict comparison for other types - } - - public function rangeSearch(BPlusTree $tree, mixed $start, mixed $end): array - { - $result = []; - $current = $tree->getRoot(); - - // Find the leaf node where the range starts - while ($current instanceof BPlusTreeInternalNode) { - $i = 0; - while ($i < count($current->keys) && $start > $current->keys[$i]) { - ++$i; - } - $current = $current->children[$i]; - } - - // Collect all values in the range - /** @var BPlusTreeLeafNode $current */ - while (null !== $current) { - foreach ($current->values as $key => $value) { - if ($current->keys[$key] >= $start && $current->keys[$key] <= $end) { - $result[] = $value; - } - if ($current->keys[$key] > $end) { - return $result; - } - } - $current = $current->next; - } - - return $result; - } -} diff --git a/tests/Tree/BPlusTreeNode/BPlusTreeInternalNodeTest.php b/tests/Tree/BPlusTreeNode/BPlusTreeInternalNodeTest.php deleted file mode 100644 index 2156243..0000000 --- a/tests/Tree/BPlusTreeNode/BPlusTreeInternalNodeTest.php +++ /dev/null @@ -1,74 +0,0 @@ -insert(10, 'value10'); - $leafNode1->insert(20, 'value20'); - $leafNode2->insert(30, 'value30'); - $leafNode2->insert(40, 'value40'); - - $internalNode->keys = [30]; - $internalNode->children = [$leafNode1, $leafNode2]; - - /** @var BPlusTreeInternalNode $result */ - $result = $internalNode->insert(25, 'value25'); - - $this->assertInstanceOf(BPlusTreeInternalNode::class, $result); - $this->assertSame([25, 30], $result->keys); - $this->assertCount(3, $result->children); - } - - public function testRemove(): void - { - $internalNode = new BPlusTreeInternalNode(4); - $leafNode1 = new BPlusTreeLeafNode(4); - $leafNode2 = new BPlusTreeLeafNode(4); - - $leafNode1->insert(10, 'value10'); - $leafNode1->insert(20, 'value20'); - $leafNode2->insert(30, 'value30'); - $leafNode2->insert(40, 'value40'); - - $internalNode->keys = [30]; - $internalNode->children = [$leafNode1, $leafNode2]; - - $result = $internalNode->remove(20); - $this->assertTrue($result); - - $this->assertSame([30], $internalNode->keys); - $this->assertCount(2, $internalNode->children); - $this->assertSame([10], $internalNode->children[0]->keys); - } - - public function testSearch(): void - { - $internalNode = new BPlusTreeInternalNode(4); - $leafNode1 = new BPlusTreeLeafNode(4); - $leafNode2 = new BPlusTreeLeafNode(4); - - $leafNode1->insert(10, 'value10'); - $leafNode1->insert(20, 'value20'); - $leafNode2->insert(30, 'value30'); - $leafNode2->insert(40, 'value40'); - - $internalNode->keys = [30]; - $internalNode->children = [$leafNode1, $leafNode2]; - - $result = $internalNode->search(30); - $this->assertSame('value30', $result); - } -} diff --git a/tests/Tree/BPlusTreeNode/BPlusTreeLeafNodeTest.php b/tests/Tree/BPlusTreeNode/BPlusTreeLeafNodeTest.php deleted file mode 100644 index 5c53884..0000000 --- a/tests/Tree/BPlusTreeNode/BPlusTreeLeafNodeTest.php +++ /dev/null @@ -1,92 +0,0 @@ -insert(10, 'value10'); - $leafNode->insert(20, 'value20'); - - $this->assertSame([10, 20], $leafNode->keys); - $this->assertSame(['value10', 'value20'], $leafNode->values); - } - - public function testInsertDuplicates(): void - { - $leafNode = new BPlusTreeLeafNode(4); - $leafNode->insert(10, 'value10'); - $leafNode->insert(10, 'newValue10'); - - $this->assertSame('newValue10', $leafNode->search(10)); - } - - public function testInsertAndSearch() - { - $order = 4; - $leafNode = new BPlusTreeLeafNode($order); - - // Inserindo chaves e valores - $leafNode->insert(10, 'value10'); - $leafNode->insert(20, 'value20'); - $leafNode->insert(30, 'value30'); - - // Testando busca - $this->assertEquals('value10', $leafNode->search(10)); - $this->assertEquals('value20', $leafNode->search(20)); - $this->assertNull($leafNode->search(40)); - } - - public function testRemove(): void - { - $leafNode = new BPlusTreeLeafNode(4); - $leafNode->insert(10, 'value10'); - $leafNode->insert(20, 'value20'); - - $result = $leafNode->remove(10); - - $this->assertTrue($result); - $this->assertSame([20], $leafNode->keys); - $this->assertSame(['value20'], $leafNode->values); - } - - public function testRemoveNonExistent(): void - { - $leafNode = new BPlusTreeLeafNode(4); - $leafNode->insert(10, 'value10'); - - $result = $leafNode->remove(20); - - $this->assertFalse($result); - $this->assertSame([10], $leafNode->keys); - $this->assertSame(['value10'], $leafNode->values); - } - - public function testSearch(): void - { - $leafNode = new BPlusTreeLeafNode(4); - $leafNode->insert(10, 'value10'); - $leafNode->insert(20, 'value20'); - - $result = $leafNode->search(20); - - $this->assertSame('value20', $result); - } - - public function testSearchNonExistent(): void - { - $leafNode = new BPlusTreeLeafNode(4); - $leafNode->insert(10, 'value10'); - - $result = $leafNode->search(20); - - $this->assertNull($result); - } -} diff --git a/tests/Tree/BPlusTreeSearcherTest.php b/tests/Tree/BPlusTreeSearcherTest.php deleted file mode 100644 index 46b3ba6..0000000 --- a/tests/Tree/BPlusTreeSearcherTest.php +++ /dev/null @@ -1,221 +0,0 @@ -tree = new BPlusTree(4); // Using order 4 for the tree - $this->searcher = new BPlusTreeSearcher(); - } - - public function testFindByKey(): void - { - // Insert elements into the tree - $this->tree->insert(10, 'Value10'); - $this->tree->insert(20, 'Value20'); - $this->tree->insert(30, 'Value30'); - $this->tree->insert(40, 'Value40'); - - // Test finding existing keys - $result = $this->searcher->find($this->tree, 20); - $this->assertEquals('Value20', $result); - - $result = $this->searcher->find($this->tree, 40); - $this->assertEquals('Value40', $result); - - // Test finding a non-existing key - $result = $this->searcher->find($this->tree, 50); - $this->assertNull($result); - } - - public function testFindByValue(): void - { - // Insert elements into the tree - $this->tree->insert(5, 'Value5'); - $this->tree->insert(15, 'Value15'); - $this->tree->insert(25, 'Value25'); - $this->tree->insert(35, 'Value35'); - - // Test finding existing values - $result = $this->searcher->find($this->tree, 'Value15'); - $this->assertEquals(15, $result); - - $result = $this->searcher->find($this->tree, 'Value35'); - $this->assertEquals(35, $result); - - // Test finding a non-existing value - $result = $this->searcher->find($this->tree, 'Value50'); - $this->assertNull($result); - } - - public function testRangeSearch(): void - { - // Insert elements into the tree - for ($i = 1; $i <= 20; ++$i) { - $this->tree->insert($i * 5, 'Value' . ($i * 5)); - } - - // Perform a range search - $result = $this->searcher->rangeSearch($this->tree, 30, 70); - - // Expected values are from 30 to 70 (inclusive) - $expected = ['Value30', 'Value35', 'Value40', 'Value45', 'Value50', 'Value55', 'Value60', 'Value65', 'Value70']; - - $this->assertEquals($expected, $result); - - // Test an empty range - $result = $this->searcher->rangeSearch($this->tree, 105, 110); - $this->assertEmpty($result); - } - - public function testSearchInLeafNode(): void - { - // Directly test the private method searchInLeafNode using reflection - $leafNode = new BPlusTreeLeafNode(4); - $leafNode->keys = [10, 20, 30]; - $leafNode->values = ['Value10', 'Value20', 'Value30']; - - // Use reflection to access the private method - $reflection = new ReflectionClass(BPlusTreeSearcher::class); - $method = $reflection->getMethod('searchInLeafNode'); - $method->setAccessible(true); - - // Test finding an existing value - $index = $method->invokeArgs($this->searcher, [$leafNode, 'Value20']); - $this->assertEquals(20, $index); - - // Test finding a non-existing value - $index = $method->invokeArgs($this->searcher, [$leafNode, 'Value50']); - $this->assertNull($index); - } - - public function testCompareValues(): void - { - // Directly test the private method compareValues using reflection - $reflection = new ReflectionClass(BPlusTreeSearcher::class); - $method = $reflection->getMethod('compareValues'); - $method->setAccessible(true); - - // Test comparison of scalars - $result = $method->invokeArgs($this->searcher, [10, 10]); - $this->assertTrue($result); - - $result = $method->invokeArgs($this->searcher, [10, 20]); - $this->assertFalse($result); - - // Test comparison of objects - $obj1 = (object) ['a' => 1]; - $obj2 = (object) ['a' => 1]; - $obj3 = (object) ['a' => 2]; - - $result = $method->invokeArgs($this->searcher, [$obj1, $obj2]); - $this->assertTrue($result); - - $result = $method->invokeArgs($this->searcher, [$obj1, $obj3]); - $this->assertFalse($result); - } - - public function testFindFirstLeafNode(): void - { - // Create a tree with multiple levels - for ($i = 1; $i <= 50; ++$i) { - $this->tree->insert($i, "Value$i"); - } - - // Use reflection to access the private method - $reflection = new ReflectionClass(BPlusTreeSearcher::class); - $method = $reflection->getMethod('findFirstLeafNode'); - $method->setAccessible(true); - - $firstLeaf = $method->invokeArgs($this->searcher, [$this->tree->getRoot()]); - $this->assertInstanceOf(BPlusTreeLeafNode::class, $firstLeaf); - - // The first leaf node should contain the smallest keys - $this->assertContains(1, $firstLeaf->keys); - } - - public function testSearchByValueWithNonExistingValue(): void - { - // Insert elements into the tree - $this->tree->insert(100, 'Value100'); - $this->tree->insert(200, 'Value200'); - - // Test searching for a non-existing value - $result = $this->searcher->find($this->tree, 'NonExistingValue'); - $this->assertNull($result); - } - - public function testSearchWithEmptyTree(): void - { - // Create an empty tree - $emptyTree = new BPlusTree(4); - - // Test searching in an empty tree - $result = $this->searcher->find($emptyTree, 10); - $this->assertNull($result); - - $result = $this->searcher->rangeSearch($emptyTree, 10, 20); - $this->assertEmpty($result); - } - - public function testFindWithNullValue(): void - { - // Insert elements with null values - $this->tree->insert(1, null); - $this->tree->insert(2, 'Value2'); - - // Test finding a key with a null value - $result = $this->searcher->find($this->tree, null); - $this->assertEquals(1, $result); - } - - public function testSearchWithDuplicateValues(): void - { - // Insert elements with duplicate values - $this->tree->insert(1, 'DuplicateValue'); - $this->tree->insert(2, 'DuplicateValue'); - $this->tree->insert(3, 'UniqueValue'); - - // Test finding by value (should return the first key that matches) - $result = $this->searcher->find($this->tree, 'DuplicateValue'); - $this->assertEquals(1, $result); - - // Test that the search continues correctly after duplicates - $result = $this->searcher->find($this->tree, 'UniqueValue'); - $this->assertEquals(3, $result); - } - - public function testRangeSearchWithSingleElementRange(): void - { - // Insert elements into the tree - $this->tree->insert(10, 'Value10'); - $this->tree->insert(20, 'Value20'); - - // Perform a range search where start and end are the same - $result = $this->searcher->rangeSearch($this->tree, 20, 20); - $this->assertEquals(['Value20'], $result); - } - - public function testFindWithEmptyTree(): void - { - // Create an empty tree - $emptyTree = new BPlusTree(4); - - // Ensure the root is null - $this->assertNull($emptyTree->getRoot()); - - // Test finding an element in an empty tree - $result = $this->searcher->find($emptyTree, 10); - $this->assertNull($result); - } -} diff --git a/tests/Tree/BPlusTreeTest.php b/tests/Tree/BPlusTreeTest.php deleted file mode 100644 index ee71477..0000000 --- a/tests/Tree/BPlusTreeTest.php +++ /dev/null @@ -1,238 +0,0 @@ -tree = new BPlusTree(self::ORDER); - } - - public function testInsertAndFind(): void - { - $this->tree->insert(1, 'A'); - $this->tree->insert(2, 'B'); - $this->tree->insert(3, 'C'); - - $this->assertEquals('A', $this->tree->find(1)); - $this->assertEquals('B', $this->tree->find(2)); - $this->assertEquals('C', $this->tree->find(3)); - } - - public function testInsertAndRemove(): void - { - $this->tree->insert(1, 'A'); - $this->tree->insert(2, 'B'); - $this->tree->insert(3, 'C'); - - $this->assertTrue($this->tree->remove(2)); - $this->assertNull($this->tree->find(2)); - - $this->assertTrue($this->tree->remove(1)); - $this->assertNull($this->tree->find(1)); - - $this->assertFalse($this->tree->remove(5)); // Test removing non-existent key - } - - public function testRangeSearch(): void - { - $this->tree->insert(1, 'A'); - $this->tree->insert(2, 'B'); - $this->tree->insert(3, 'C'); - $this->tree->insert(4, 'D'); - - $result = $this->tree->rangeSearch(2, 3); - $this->assertEquals(['B', 'C'], $result); - } - - public function testClearTree(): void - { - $this->tree->insert(1, 'A'); - $this->tree->insert(2, 'B'); - - $this->tree->clear(); - $this->assertNull($this->tree->getRoot()); - $this->assertEquals(0, $this->tree->size()); - } - - public function testGetItems(): void - { - $this->tree->insert(1, 'A'); - $this->tree->insert(2, 'B'); - $this->tree->insert(3, 'C'); - - $items = $this->tree->getItems(); - $this->assertEquals(['A', 'B', 'C'], $items); - } - - public function testGetOrder(): void - { - $this->assertEquals(self::ORDER, $this->tree->getOrder()); - } - - public function testGetMinimumAndMaximum(): void - { - $this->tree->insert(5, 'E'); - $this->tree->insert(1, 'A'); - $this->tree->insert(3, 'C'); - - $this->assertEquals('A', $this->tree->getMinimum()); - $this->assertEquals('E', $this->tree->getMaximum()); - } - - public function testSetAndGetByKey(): void - { - $this->tree->insert(1, 'A'); - $this->tree->insert(2, 'B'); - $this->tree->insert(3, 'C'); - - $this->tree->set(1, 'Z'); - $this->assertEquals('Z', $this->tree->get(1)); - } - - public function testVisualTreeStructure(): void - { - $this->tree->insert(1, 'A'); - $this->tree->insert(2, 'B'); - $this->tree->insert(3, 'C'); - $this->tree->insert(4, 'D'); - $this->tree->insert(5, 'E'); - - $visual = $this->tree->visualTreeStructure(); - $this->assertStringContainsString('Leaf Node', $visual); - $this->assertStringContainsString('Internal Node', $visual); - } - - public function testConstructorWithInvalidOrder(): void - { - $this->expectException(InvalidArgumentException::class); - new BPlusTree(2); // Order less than 3 should throw exception - } - - public function testGetWithInvalidIndex(): void - { - $this->expectException(OutOfRangeException::class); - $this->tree->get(99); // Attempt to access an invalid key - } - - public function testAdd(): void - { - $this->tree->add(1); - $this->assertEquals(1, $this->tree->find(1)); - - $this->tree->add(2); - $this->assertEquals(2, $this->tree->find(2)); - - $this->assertEquals(2, $this->tree->size()); - } - - public function testRemoveUntilEmpty(): void - { - $this->tree->insert(1, 'A'); - $this->tree->insert(2, 'B'); - - $this->assertTrue($this->tree->remove(1)); - $this->assertTrue($this->tree->remove(2)); - - $this->assertNull($this->tree->getRoot()); - $this->assertEquals(0, $this->tree->size()); - } - - public function testContains(): void - { - $this->tree->insert(1, 'A'); - $this->assertTrue($this->tree->contains(1)); - $this->assertFalse($this->tree->contains(2)); - } - - public function testSetWithInvalidKey(): void - { - $this->expectException(OutOfRangeException::class); - $this->tree->set(99, 'Z'); // Key 99 does not exist - } - - public function testAddAll(): void - { - /** @var Collection|MockObject */ - $mockCollection = $this->createMock(Collection::class); - $mockCollection->method('getItems')->willReturn([1, 2, 3]); - - $this->tree->addAll($mockCollection); - - $this->assertEquals(3, $this->tree->size()); - $this->assertEquals(1, $this->tree->find(1)); - $this->assertEquals(2, $this->tree->find(2)); - $this->assertEquals(3, $this->tree->find(3)); - } - - public function testIsBalanced(): void - { - for ($i = 1; $i <= 10; ++$i) { - $this->tree->insert($i, "Value$i"); - } - - $this->assertTrue($this->tree->isBalanced(), 'Tree should be balanced after insertions'); - } - - public function testSort(): void - { - $this->tree->insert(3, 'C'); - $this->tree->insert(1, 'A'); - $this->tree->insert(2, 'B'); - - // Calling sort to check if the tree is sorted - $this->tree->sort(); - - // If no exception is thrown, the tree is sorted - $this->assertTrue(true); - } - - public function testGetLeftmostLeafWithEmptyTree(): void - { - $reflection = new ReflectionClass($this->tree); - $method = $reflection->getMethod('getLeftmostLeaf'); - $method->setAccessible(true); - - $result = $method->invoke($this->tree); - $this->assertNull($result); - } - - public function testGetRightmostLeafWithEmptyTree(): void - { - $reflection = new ReflectionClass($this->tree); - $method = $reflection->getMethod('getRightmostLeaf'); - $method->setAccessible(true); - - $result = $method->invoke($this->tree); - $this->assertNull($result); - } - - public function testVisualTreeStructureWithEmptyTree(): void - { - $visual = $this->tree->visualTreeStructure(); - $this->assertEquals('Empty tree', $visual); - } - - public function testRemoveNonExistentKey(): void - { - $this->tree->insert(1, 'A'); - $this->assertFalse($this->tree->remove(99)); - } - - public function testSetExistingKey(): void - { - $this->tree->insert(1, 'A'); - $this->tree->set(1, 'Z'); - $this->assertEquals('Z', $this->tree->get(1)); - } -}