Skip to content

Commit 25e0a07

Browse files
committed
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.'
1 parent a547e33 commit 25e0a07

File tree

5 files changed

+386
-47
lines changed

5 files changed

+386
-47
lines changed

src/Tree/BPlusTree.php

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ public function __construct(private int $order)
4747
if ($order < 3) {
4848
throw new \InvalidArgumentException('Order must be at least 3');
4949
}
50-
$this->searcher = new BPlusTreeSearcher();
5150
$this->order = $order;
52-
$this->root = new BPlusTreeLeafNode($order);
51+
$this->searcher = new BPlusTreeSearcher();
52+
$this->root = null;
5353
}
5454

5555
public function getRoot(): ?BPlusTreeNode
@@ -64,6 +64,10 @@ public function add(mixed $element): void
6464

6565
public function insert(int $key, mixed $value): void
6666
{
67+
if (null === $this->root) {
68+
$this->root = new BPlusTreeLeafNode($this->order);
69+
}
70+
6771
$newRoot = $this->root->insert($key, $value);
6872
if ($newRoot !== $this->root) {
6973
$this->root = $newRoot;
@@ -83,6 +87,8 @@ public function remove(mixed $element): bool
8387
--$this->size;
8488
if ($this->root instanceof BPlusTreeInternalNode && 0 === count($this->root->keys)) {
8589
$this->root = $this->root->children[0];
90+
} elseif ($this->root instanceof BPlusTreeLeafNode && 0 === count($this->root->keys)) {
91+
$this->root = null; // Set root to null when tree is empty
8692
}
8793
}
8894

@@ -122,6 +128,10 @@ public function get(int $key): mixed
122128

123129
public function set(int $key, mixed $value): void
124130
{
131+
if (null === $this->root) {
132+
throw new \OutOfRangeException('Key not found');
133+
}
134+
125135
$node = $this->root;
126136
while ($node instanceof BPlusTreeInternalNode) {
127137
$index = 0;
@@ -131,6 +141,10 @@ public function set(int $key, mixed $value): void
131141
$node = $node->children[$index];
132142
}
133143

144+
if (null === $node) {
145+
throw new \OutOfRangeException('Key not found');
146+
}
147+
134148
/** @var BPlusTreeLeafNode $node */
135149
$index = array_search($key, $node->keys);
136150
if (false !== $index) {
@@ -196,30 +210,35 @@ public function isBalanced(): bool
196210
return true;
197211
}
198212

199-
return false !== $this->checkBalance($this->root);
213+
$leafDepth = null;
214+
215+
return $this->checkBalance($this->root, 0, $leafDepth);
200216
}
201217

202-
private function checkBalance(?BPlusTreeNode $node): int
218+
private function checkBalance(?BPlusTreeNode $node, int $currentDepth = 0, ?int &$leafDepth = null): bool
203219
{
204220
if (null === $node) {
205-
return 0;
221+
return true;
206222
}
207223

208224
if ($node instanceof BPlusTreeLeafNode) {
209-
return 1;
225+
if (null === $leafDepth) {
226+
$leafDepth = $currentDepth;
227+
} elseif ($currentDepth !== $leafDepth) {
228+
return false;
229+
}
230+
231+
return true;
210232
}
211233

212234
/** @var BPlusTreeInternalNode $node */
213-
$height = $this->checkBalance($node->children[0]);
214-
215-
for ($i = 1; $i < count($node->children); ++$i) {
216-
$childHeight = $this->checkBalance($node->children[$i]);
217-
if ($childHeight !== $height) {
218-
throw new \RuntimeException('B+ Tree is not balanced');
235+
foreach ($node->children as $child) {
236+
if (! $this->checkBalance($child, $currentDepth + 1, $leafDepth)) {
237+
return false;
219238
}
220239
}
221240

222-
return $height + 1;
241+
return true;
223242
}
224243

225244
public function sort(): void

src/Tree/BPlusTreeNode/BPlusTreeInternalNode.php

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,30 +51,27 @@ private function findIndex(int $key): int
5151

5252
private function split(): BPlusTreeInternalNode
5353
{
54-
$order = $this->order;
55-
$middleKeyIndex = (int) (($order - 1) / 2);
54+
$numKeys = count($this->keys);
55+
$middleKeyIndex = intdiv($numKeys, 2);
5656
$middleKey = $this->keys[$middleKeyIndex];
5757

58-
// Create left and right nodes
59-
$leftNode = new BPlusTreeInternalNode($order);
60-
$rightNode = new BPlusTreeInternalNode($order);
58+
$leftNode = new BPlusTreeInternalNode($this->order);
59+
$rightNode = new BPlusTreeInternalNode($this->order);
6160

62-
// Left node keys and children
6361
$leftNode->keys = array_slice($this->keys, 0, $middleKeyIndex);
6462
$leftNode->children = array_slice($this->children, 0, $middleKeyIndex + 1);
6563

66-
// Right node keys and children
6764
$rightNode->keys = array_slice($this->keys, $middleKeyIndex + 1);
6865
$rightNode->children = array_slice($this->children, $middleKeyIndex + 1);
6966

70-
// Create a new parent node and promote the middle key
71-
$parent = new BPlusTreeInternalNode($order);
67+
$parent = new BPlusTreeInternalNode($this->order);
7268
$parent->keys = [$middleKey];
7369
$parent->children = [$leftNode, $rightNode];
7470

7571
return $parent;
7672
}
7773

74+
7875
public function search(mixed $key): mixed
7976
{
8077
$index = $this->findInsertionIndex($key);

src/Tree/BPlusTreeNode/BPlusTreeLeafNode.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ private function findIndex(int $key): int
4141

4242
private function split(): BPlusTreeInternalNode
4343
{
44-
$order = $this->order;
45-
$middleIndex = (int) ($order / 2);
46-
$newNode = new BPlusTreeLeafNode($order);
44+
$numKeys = count($this->keys);
45+
$middleIndex = intdiv($numKeys, 2);
46+
$newNode = new BPlusTreeLeafNode($this->order);
4747

4848
$newNode->keys = array_slice($this->keys, $middleIndex);
4949
$newNode->values = array_slice($this->values, $middleIndex);
@@ -54,7 +54,7 @@ private function split(): BPlusTreeInternalNode
5454
$newNode->next = $this->next;
5555
$this->next = $newNode;
5656

57-
$parent = new BPlusTreeInternalNode($order);
57+
$parent = new BPlusTreeInternalNode($this->order);
5858
$parent->keys = [$newNode->keys[0]];
5959
$parent->children = [$this, $newNode];
6060

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use KaririCode\DataStructure\Tree\BPlusTree;
6+
use KaririCode\DataStructure\Tree\BPlusTreeNode\BPlusTreeLeafNode;
7+
use KaririCode\DataStructure\Tree\BPlusTreeSearcher;
8+
use PHPUnit\Framework\TestCase;
9+
10+
final class BPlusTreeSearcherTest extends TestCase
11+
{
12+
private BPlusTree $tree;
13+
private BPlusTreeSearcher $searcher;
14+
15+
protected function setUp(): void
16+
{
17+
$this->tree = new BPlusTree(4); // Using order 4 for the tree
18+
$this->searcher = new BPlusTreeSearcher();
19+
}
20+
21+
public function testFindByKey(): void
22+
{
23+
// Insert elements into the tree
24+
$this->tree->insert(10, 'Value10');
25+
$this->tree->insert(20, 'Value20');
26+
$this->tree->insert(30, 'Value30');
27+
$this->tree->insert(40, 'Value40');
28+
29+
// Test finding existing keys
30+
$result = $this->searcher->find($this->tree, 20);
31+
$this->assertEquals('Value20', $result);
32+
33+
$result = $this->searcher->find($this->tree, 40);
34+
$this->assertEquals('Value40', $result);
35+
36+
// Test finding a non-existing key
37+
$result = $this->searcher->find($this->tree, 50);
38+
$this->assertNull($result);
39+
}
40+
41+
public function testFindByValue(): void
42+
{
43+
// Insert elements into the tree
44+
$this->tree->insert(5, 'Value5');
45+
$this->tree->insert(15, 'Value15');
46+
$this->tree->insert(25, 'Value25');
47+
$this->tree->insert(35, 'Value35');
48+
49+
// Test finding existing values
50+
$result = $this->searcher->find($this->tree, 'Value15');
51+
$this->assertEquals(15, $result);
52+
53+
$result = $this->searcher->find($this->tree, 'Value35');
54+
$this->assertEquals(35, $result);
55+
56+
// Test finding a non-existing value
57+
$result = $this->searcher->find($this->tree, 'Value50');
58+
$this->assertNull($result);
59+
}
60+
61+
public function testRangeSearch(): void
62+
{
63+
// Insert elements into the tree
64+
for ($i = 1; $i <= 20; ++$i) {
65+
$this->tree->insert($i * 5, 'Value' . ($i * 5));
66+
}
67+
68+
// Perform a range search
69+
$result = $this->searcher->rangeSearch($this->tree, 30, 70);
70+
71+
// Expected values are from 30 to 70 (inclusive)
72+
$expected = ['Value30', 'Value35', 'Value40', 'Value45', 'Value50', 'Value55', 'Value60', 'Value65', 'Value70'];
73+
74+
$this->assertEquals($expected, $result);
75+
76+
// Test an empty range
77+
$result = $this->searcher->rangeSearch($this->tree, 105, 110);
78+
$this->assertEmpty($result);
79+
}
80+
81+
public function testSearchInLeafNode(): void
82+
{
83+
// Directly test the private method searchInLeafNode using reflection
84+
$leafNode = new BPlusTreeLeafNode(4);
85+
$leafNode->keys = [10, 20, 30];
86+
$leafNode->values = ['Value10', 'Value20', 'Value30'];
87+
88+
// Use reflection to access the private method
89+
$reflection = new ReflectionClass(BPlusTreeSearcher::class);
90+
$method = $reflection->getMethod('searchInLeafNode');
91+
$method->setAccessible(true);
92+
93+
// Test finding an existing value
94+
$index = $method->invokeArgs($this->searcher, [$leafNode, 'Value20']);
95+
$this->assertEquals(20, $index);
96+
97+
// Test finding a non-existing value
98+
$index = $method->invokeArgs($this->searcher, [$leafNode, 'Value50']);
99+
$this->assertNull($index);
100+
}
101+
102+
public function testCompareValues(): void
103+
{
104+
// Directly test the private method compareValues using reflection
105+
$reflection = new ReflectionClass(BPlusTreeSearcher::class);
106+
$method = $reflection->getMethod('compareValues');
107+
$method->setAccessible(true);
108+
109+
// Test comparison of scalars
110+
$result = $method->invokeArgs($this->searcher, [10, 10]);
111+
$this->assertTrue($result);
112+
113+
$result = $method->invokeArgs($this->searcher, [10, 20]);
114+
$this->assertFalse($result);
115+
116+
// Test comparison of objects
117+
$obj1 = (object) ['a' => 1];
118+
$obj2 = (object) ['a' => 1];
119+
$obj3 = (object) ['a' => 2];
120+
121+
$result = $method->invokeArgs($this->searcher, [$obj1, $obj2]);
122+
$this->assertTrue($result);
123+
124+
$result = $method->invokeArgs($this->searcher, [$obj1, $obj3]);
125+
$this->assertFalse($result);
126+
}
127+
128+
public function testFindFirstLeafNode(): void
129+
{
130+
// Create a tree with multiple levels
131+
for ($i = 1; $i <= 50; ++$i) {
132+
$this->tree->insert($i, "Value$i");
133+
}
134+
135+
// Use reflection to access the private method
136+
$reflection = new ReflectionClass(BPlusTreeSearcher::class);
137+
$method = $reflection->getMethod('findFirstLeafNode');
138+
$method->setAccessible(true);
139+
140+
$firstLeaf = $method->invokeArgs($this->searcher, [$this->tree->getRoot()]);
141+
$this->assertInstanceOf(BPlusTreeLeafNode::class, $firstLeaf);
142+
143+
// The first leaf node should contain the smallest keys
144+
$this->assertContains(1, $firstLeaf->keys);
145+
}
146+
147+
public function testSearchByValueWithNonExistingValue(): void
148+
{
149+
// Insert elements into the tree
150+
$this->tree->insert(100, 'Value100');
151+
$this->tree->insert(200, 'Value200');
152+
153+
// Test searching for a non-existing value
154+
$result = $this->searcher->find($this->tree, 'NonExistingValue');
155+
$this->assertNull($result);
156+
}
157+
158+
public function testSearchWithEmptyTree(): void
159+
{
160+
// Create an empty tree
161+
$emptyTree = new BPlusTree(4);
162+
163+
// Test searching in an empty tree
164+
$result = $this->searcher->find($emptyTree, 10);
165+
$this->assertNull($result);
166+
167+
$result = $this->searcher->rangeSearch($emptyTree, 10, 20);
168+
$this->assertEmpty($result);
169+
}
170+
171+
public function testFindWithNullValue(): void
172+
{
173+
// Insert elements with null values
174+
$this->tree->insert(1, null);
175+
$this->tree->insert(2, 'Value2');
176+
177+
// Test finding a key with a null value
178+
$result = $this->searcher->find($this->tree, null);
179+
$this->assertEquals(1, $result);
180+
}
181+
182+
public function testSearchWithDuplicateValues(): void
183+
{
184+
// Insert elements with duplicate values
185+
$this->tree->insert(1, 'DuplicateValue');
186+
$this->tree->insert(2, 'DuplicateValue');
187+
$this->tree->insert(3, 'UniqueValue');
188+
189+
// Test finding by value (should return the first key that matches)
190+
$result = $this->searcher->find($this->tree, 'DuplicateValue');
191+
$this->assertEquals(1, $result);
192+
193+
// Test that the search continues correctly after duplicates
194+
$result = $this->searcher->find($this->tree, 'UniqueValue');
195+
$this->assertEquals(3, $result);
196+
}
197+
198+
public function testRangeSearchWithSingleElementRange(): void
199+
{
200+
// Insert elements into the tree
201+
$this->tree->insert(10, 'Value10');
202+
$this->tree->insert(20, 'Value20');
203+
204+
// Perform a range search where start and end are the same
205+
$result = $this->searcher->rangeSearch($this->tree, 20, 20);
206+
$this->assertEquals(['Value20'], $result);
207+
}
208+
209+
public function testFindWithEmptyTree(): void
210+
{
211+
// Create an empty tree
212+
$emptyTree = new BPlusTree(4);
213+
214+
// Ensure the root is null
215+
$this->assertNull($emptyTree->getRoot());
216+
217+
// Test finding an element in an empty tree
218+
$result = $this->searcher->find($emptyTree, 10);
219+
$this->assertNull($result);
220+
}
221+
}

0 commit comments

Comments
 (0)