pkstruct 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. pkstruct/__init__.py +167 -0
  2. pkstruct/graphs/__init__.py +127 -0
  3. pkstruct/graphs/connectivity.py +157 -0
  4. pkstruct/graphs/directed.py +95 -0
  5. pkstruct/graphs/exceptions.py +63 -0
  6. pkstruct/graphs/graph.py +262 -0
  7. pkstruct/graphs/mst.py +118 -0
  8. pkstruct/graphs/scc.py +138 -0
  9. pkstruct/graphs/shortest_path.py +250 -0
  10. pkstruct/graphs/topo_sort.py +108 -0
  11. pkstruct/graphs/traversal.py +175 -0
  12. pkstruct/graphs/visualization.py +90 -0
  13. pkstruct/graphs/weighted.py +37 -0
  14. pkstruct/linear/__init__.py +95 -0
  15. pkstruct/linear/deques/__init__.py +33 -0
  16. pkstruct/linear/deques/deque.py +194 -0
  17. pkstruct/linear/deques/linked_deque.py +198 -0
  18. pkstruct/linear/exceptions.py +26 -0
  19. pkstruct/linear/linked_lists/__init__.py +5 -0
  20. pkstruct/linear/linked_lists/_base.py +608 -0
  21. pkstruct/linear/linked_lists/circular_linked_list.py +230 -0
  22. pkstruct/linear/linked_lists/doubly_linked_list.py +151 -0
  23. pkstruct/linear/linked_lists/nodes.py +68 -0
  24. pkstruct/linear/linked_lists/singly_linked_list.py +136 -0
  25. pkstruct/linear/queues/__init__.py +44 -0
  26. pkstruct/linear/queues/circular_queue.py +258 -0
  27. pkstruct/linear/queues/linked_queue.py +186 -0
  28. pkstruct/linear/queues/priority_queue.py +202 -0
  29. pkstruct/linear/queues/queue.py +174 -0
  30. pkstruct/linear/stacks/__init__.py +38 -0
  31. pkstruct/linear/stacks/array_stack.py +165 -0
  32. pkstruct/linear/stacks/linked_stack.py +168 -0
  33. pkstruct/linear/stacks/stack.py +158 -0
  34. pkstruct/linear/utils/__init__.py +18 -0
  35. pkstruct/linear/utils/benchmark.py +255 -0
  36. pkstruct/linear/utils/debug_tools.py +239 -0
  37. pkstruct/linear/utils/helpers.py +143 -0
  38. pkstruct/linear/utils/iterators.py +148 -0
  39. pkstruct/linear/visualization/__init__.py +0 -0
  40. pkstruct/linear/visualization/ascii_visualizer.py +114 -0
  41. pkstruct/linear/visualization/linked_list_visualizer.py +126 -0
  42. pkstruct/shared/__init__.py +67 -0
  43. pkstruct/shared/benchmarking/__init__.py +78 -0
  44. pkstruct/shared/debugging/__init__.py +69 -0
  45. pkstruct/shared/exceptions/__init__.py +59 -0
  46. pkstruct/shared/serializers/__init__.py +65 -0
  47. pkstruct/shared/threading/__init__.py +43 -0
  48. pkstruct/shared/validators/__init__.py +98 -0
  49. pkstruct/shared/visualization/__init__.py +21 -0
  50. pkstruct/trees/__init__.py +92 -0
  51. pkstruct/trees/avl.py +321 -0
  52. pkstruct/trees/balancing.py +253 -0
  53. pkstruct/trees/bplus.py +425 -0
  54. pkstruct/trees/bst.py +948 -0
  55. pkstruct/trees/btree.py +504 -0
  56. pkstruct/trees/exceptions.py +96 -0
  57. pkstruct/trees/fenwick_tree.py +312 -0
  58. pkstruct/trees/interval_tree.py +541 -0
  59. pkstruct/trees/node.py +356 -0
  60. pkstruct/trees/red_black.py +710 -0
  61. pkstruct/trees/segment_tree.py +398 -0
  62. pkstruct/trees/traversal.py +456 -0
  63. pkstruct/trees/tree_helpers.py +366 -0
  64. pkstruct/trees/utils/__init__.py +15 -0
  65. pkstruct/trees/utils/complexity_helpers.py +231 -0
  66. pkstruct/trees/visualization/__init__.py +0 -0
  67. pkstruct/trees/visualization/ascii_renderer.py +220 -0
  68. pkstruct/trees/visualization/tree_printer.py +129 -0
  69. pkstruct-0.1.0.dist-info/METADATA +482 -0
  70. pkstruct-0.1.0.dist-info/RECORD +72 -0
  71. pkstruct-0.1.0.dist-info/WHEEL +4 -0
  72. pkstruct-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,253 @@
1
+ """
2
+ pkstruct.trees.balancing
3
+ ========================
4
+ Shared balancing utilities used by :class:`~pkstruct.trees.avl.AVLTree`
5
+ and :class:`~pkstruct.trees.red_black.RedBlackTree`.
6
+
7
+ Design goals
8
+ ------------
9
+ * **Single** ``rotate`` function accepts a ``direction`` parameter instead
10
+ of separate ``rotate_left`` / ``rotate_right`` methods.
11
+ * ``update_metadata`` is dispatched on node type so AVL and RB nodes each
12
+ maintain their own invariants without branching inside tree classes.
13
+ * All functions operate on nodes directly and return the new sub-tree root
14
+ when the root may change; callers are responsible for re-attaching.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from .node import AVLNode, RBNode, TreeNode
20
+
21
+ __all__ = [
22
+ "rotate",
23
+ "rebalance",
24
+ "update_metadata",
25
+ "get_balance_factor",
26
+ "validate_balance",
27
+ ]
28
+
29
+ _AnyNode = AVLNode | RBNode | TreeNode
30
+
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # Height helpers (AVL)
34
+ # ---------------------------------------------------------------------------
35
+
36
+
37
+ def _height(node: AVLNode | None) -> int:
38
+ """Return the height of *node*, or 0 if *None*."""
39
+ return node.height if node is not None else 0
40
+
41
+
42
+ def _update_height(node: AVLNode) -> None:
43
+ """Recalculate ``node.height`` from its children."""
44
+ node.height = 1 + max(_height(node.left), _height(node.right)) # type: ignore[arg-type]
45
+
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Core rotation
49
+ # ---------------------------------------------------------------------------
50
+
51
+
52
+ def rotate(
53
+ node: _AnyNode,
54
+ direction: str,
55
+ root: _AnyNode | None = None,
56
+ ) -> _AnyNode:
57
+ """
58
+ Perform a single rotation on *node* and return the new sub-tree root.
59
+
60
+ Parameters
61
+ ----------
62
+ node:
63
+ The node to rotate around (the "pivot's parent").
64
+ direction:
65
+ ``"left"`` or ``"right"``.
66
+ root:
67
+ The overall tree root. When *node* **is** the tree root the
68
+ caller must replace ``tree.root`` with the returned value.
69
+
70
+ Returns
71
+ -------
72
+ _AnyNode
73
+ The new sub-tree root after rotation.
74
+
75
+ Raises
76
+ ------
77
+ ValueError
78
+ If *direction* is not ``"left"`` or ``"right"``.
79
+ ValueError
80
+ If the required child is absent (cannot rotate).
81
+ """
82
+ if direction not in ("left", "right"):
83
+ raise ValueError(f"direction must be 'left' or 'right', got {direction!r}.")
84
+
85
+ if direction == "left":
86
+ pivot = node.right
87
+ if pivot is None:
88
+ raise ValueError("Cannot rotate left: node has no right child.")
89
+ node.right = pivot.left
90
+ if pivot.left is not None and pivot.left.key is not None:
91
+ pivot.left.parent = node
92
+ pivot.parent = node.parent
93
+ if node.parent is None:
94
+ pass # caller updates root
95
+ elif node.parent.left is node:
96
+ node.parent.left = pivot
97
+ else:
98
+ node.parent.right = pivot
99
+ pivot.left = node
100
+ node.parent = pivot
101
+ else: # right
102
+ pivot = node.left
103
+ if pivot is None:
104
+ raise ValueError("Cannot rotate right: node has no left child.")
105
+ node.left = pivot.right
106
+ if pivot.right is not None and pivot.right.key is not None:
107
+ pivot.right.parent = node
108
+ pivot.parent = node.parent
109
+ if node.parent is None:
110
+ pass
111
+ elif node.parent.left is node:
112
+ node.parent.left = pivot
113
+ else:
114
+ node.parent.right = pivot
115
+ pivot.right = node
116
+ node.parent = pivot
117
+
118
+ # Update heights for AVL nodes
119
+ if isinstance(node, AVLNode):
120
+ _update_height(node)
121
+ _update_height(pivot) # type: ignore[arg-type]
122
+
123
+ return pivot # type: ignore[return-value]
124
+
125
+
126
+ # ---------------------------------------------------------------------------
127
+ # Metadata update dispatch
128
+ # ---------------------------------------------------------------------------
129
+
130
+
131
+ def update_metadata(node: _AnyNode) -> None:
132
+ """
133
+ Recompute cached metadata stored on *node*.
134
+
135
+ For :class:`~pkstruct.trees.node.AVLNode`: updates ``height``.
136
+ For other node types: no-op (override as needed).
137
+
138
+ Parameters
139
+ ----------
140
+ node:
141
+ Node whose metadata should be refreshed.
142
+ """
143
+ if isinstance(node, AVLNode):
144
+ _update_height(node)
145
+
146
+
147
+ # ---------------------------------------------------------------------------
148
+ # Balance factor
149
+ # ---------------------------------------------------------------------------
150
+
151
+
152
+ def get_balance_factor(node: AVLNode | None) -> int: # type: ignore[return]
153
+ """
154
+ Return the balance factor of *node* (left height − right height).
155
+
156
+ Parameters
157
+ ----------
158
+ node:
159
+ An :class:`~pkstruct.trees.node.AVLNode`, or *None*.
160
+
161
+ Returns
162
+ -------
163
+ int
164
+ Balance factor; **0** when *node* is *None*.
165
+ """
166
+ if node is None:
167
+ return 0
168
+ return _height(node.left) - _height(node.right) # type: ignore[arg-type]
169
+
170
+
171
+ # ---------------------------------------------------------------------------
172
+ # AVL rebalance
173
+ # ---------------------------------------------------------------------------
174
+
175
+
176
+ def rebalance(node: AVLNode, strategy: str = "avl") -> AVLNode:
177
+ """
178
+ Restore balance at *node* using the requested *strategy*.
179
+
180
+ Currently the only supported strategy is ``"avl"``.
181
+
182
+ Parameters
183
+ ----------
184
+ node:
185
+ Node to rebalance.
186
+ strategy:
187
+ Balancing strategy identifier (currently only ``"avl"``).
188
+
189
+ Returns
190
+ -------
191
+ AVLNode
192
+ The new sub-tree root after rebalancing (may differ from *node*).
193
+
194
+ Raises
195
+ ------
196
+ ValueError
197
+ If *strategy* is not recognised.
198
+ """
199
+ if strategy != "avl":
200
+ raise ValueError(f"Unknown rebalancing strategy: {strategy!r}.")
201
+
202
+ _update_height(node)
203
+ balance = get_balance_factor(node)
204
+
205
+ # Left heavy
206
+ if balance > 1:
207
+ left_child: AVLNode = node.left # type: ignore[assignment]
208
+ if get_balance_factor(left_child) < 0:
209
+ # Left-Right case
210
+ node.left = rotate(left_child, "left") # type: ignore[assignment]
211
+ node.left.parent = node
212
+ return rotate(node, "right") # type: ignore[return-value]
213
+
214
+ # Right heavy
215
+ if balance < -1:
216
+ right_child: AVLNode = node.right # type: ignore[assignment]
217
+ if get_balance_factor(right_child) > 0:
218
+ # Right-Left case
219
+ node.right = rotate(right_child, "right") # type: ignore[assignment]
220
+ node.right.parent = node
221
+ return rotate(node, "left") # type: ignore[return-value]
222
+
223
+ return node
224
+
225
+
226
+ # ---------------------------------------------------------------------------
227
+ # Validation
228
+ # ---------------------------------------------------------------------------
229
+
230
+
231
+ def validate_balance(root: AVLNode | None) -> bool:
232
+ """
233
+ Recursively verify that every node satisfies the AVL balance property.
234
+
235
+ Parameters
236
+ ----------
237
+ root:
238
+ Root of the sub-tree to validate.
239
+
240
+ Returns
241
+ -------
242
+ bool
243
+ *True* when every node has ``|balance_factor| <= 1``.
244
+ """
245
+
246
+ def _check(node: AVLNode | None) -> bool:
247
+ if node is None:
248
+ return True
249
+ if abs(get_balance_factor(node)) > 1:
250
+ return False
251
+ return _check(node.left) and _check(node.right) # type: ignore[arg-type]
252
+
253
+ return _check(root)
@@ -0,0 +1,425 @@
1
+ """
2
+ pkstruct.trees.bplus
3
+ ====================
4
+ B+ Tree implementation with leaf-linked chain for efficient range queries.
5
+
6
+ Architecture
7
+ ------------
8
+ ``BPlusTree`` stores all data in the leaves. Internal nodes contain only
9
+ separator keys for routing. Leaf nodes are linked via a ``next_leaf``
10
+ pointer, enabling efficient range scans without recursive traversal.
11
+
12
+ All major operations run in O(log n). Range queries benefit from the
13
+ leaf chain after the initial O(log n) descent.
14
+
15
+ Public API
16
+ ----------
17
+ Core CRUD
18
+ insert, delete, search, contains, update, clear
19
+
20
+ Metrics
21
+ size, is_empty, min, max
22
+
23
+ Navigation
24
+ leaf_traversal
25
+
26
+ Range queries
27
+ range_query
28
+
29
+ Structural
30
+ validate
31
+
32
+ Traversal
33
+ __iter__, __reversed__
34
+
35
+ Dunder
36
+ __len__, __contains__, __iter__, __repr__
37
+
38
+ Complexity
39
+ ----------
40
+ Search O(log n)
41
+ Insert O(log n)
42
+ Delete O(log n)
43
+ Range query O(log n + k)
44
+ Space O(n)
45
+ """
46
+
47
+ from __future__ import annotations
48
+
49
+ from collections.abc import Iterator
50
+ from typing import Any
51
+
52
+ from pkstruct.shared.threading import StructureLock
53
+ from pkstruct.trees.exceptions import (
54
+ DuplicateKeyError,
55
+ EmptyTreeError,
56
+ InvalidOrderError,
57
+ KeyNotFoundError,
58
+ )
59
+ from pkstruct.trees.node import BPlusNode
60
+
61
+
62
+ class BPlusTree:
63
+ """A B+ Tree with leaf-linked chain for efficient range queries.
64
+
65
+ Parameters
66
+ ----------
67
+ order:
68
+ Minimum degree of the tree (must be >= 2). The maximum number
69
+ of keys per node is ``2 * order - 1``.
70
+ allow_duplicates:
71
+ When *False* (default) duplicate keys update the stored value.
72
+ When *True* duplicates are rejected with ``DuplicateKeyError``.
73
+ """
74
+
75
+ def __init__(self, order: int = 3, allow_duplicates: bool = False) -> None:
76
+ if order < 2:
77
+ raise InvalidOrderError(order)
78
+ self._order: int = order
79
+ self._allow_duplicates: bool = allow_duplicates
80
+ self._root: BPlusNode = BPlusNode(is_leaf=True)
81
+ self._first_leaf: BPlusNode = self._root
82
+ self._size: int = 0
83
+ self._lock: StructureLock = StructureLock()
84
+
85
+ # ------------------------------------------------------------------
86
+ # Properties
87
+ # ------------------------------------------------------------------
88
+
89
+ @property
90
+ def order(self) -> int:
91
+ """Minimum degree of this B+ Tree."""
92
+ return self._order
93
+
94
+ # ------------------------------------------------------------------
95
+ # Core CRUD
96
+ # ------------------------------------------------------------------
97
+
98
+ def insert(self, key: Any, value: Any = None) -> None:
99
+ """Insert *key* with optional *value*.
100
+
101
+ Parameters
102
+ ----------
103
+ key:
104
+ Comparable key.
105
+ value:
106
+ Optional payload.
107
+
108
+ Raises
109
+ ------
110
+ DuplicateKeyError
111
+ If ``allow_duplicates=True`` and *key* already exists.
112
+ """
113
+ with self._lock:
114
+ # Check for duplicate
115
+ try:
116
+ existing = self._find_leaf(self._root, key)
117
+ if existing is not None and key in existing.keys:
118
+ if self._allow_duplicates:
119
+ raise DuplicateKeyError(key)
120
+ idx = existing.keys.index(key)
121
+ existing.values[idx] = value
122
+ return
123
+ except (KeyNotFoundError, AttributeError):
124
+ pass
125
+
126
+ leaf = self._find_leaf(self._root, key)
127
+ self._insert_into_leaf(leaf, key, value)
128
+
129
+ if len(leaf.keys) > 2 * self._order - 1:
130
+ self._split_leaf(leaf)
131
+
132
+ def _find_leaf(self, node: BPlusNode, key: Any) -> BPlusNode:
133
+ if node.is_leaf:
134
+ return node
135
+ i = 0
136
+ while i < len(node.keys) and key >= node.keys[i]:
137
+ i += 1
138
+ return self._find_leaf(node.children[i], key)
139
+
140
+ def _insert_into_leaf(self, leaf: BPlusNode, key: Any, value: Any) -> None:
141
+ i = 0
142
+ while i < len(leaf.keys) and leaf.keys[i] < key:
143
+ i += 1
144
+ leaf.keys.insert(i, key)
145
+ leaf.values.insert(i, value)
146
+ self._size += 1
147
+
148
+ def _split_leaf(self, leaf: BPlusNode) -> None:
149
+ new_leaf = BPlusNode(is_leaf=True)
150
+ mid = len(leaf.keys) // 2
151
+
152
+ new_leaf.keys = leaf.keys[mid:]
153
+ new_leaf.values = leaf.values[mid:]
154
+ leaf.keys = leaf.keys[:mid]
155
+ leaf.values = leaf.values[:mid]
156
+
157
+ new_leaf.next_leaf = leaf.next_leaf
158
+ leaf.next_leaf = new_leaf
159
+
160
+ self._insert_into_parent(leaf, new_leaf.keys[0], new_leaf)
161
+
162
+ def _insert_into_parent(
163
+ self,
164
+ left: BPlusNode,
165
+ key: Any,
166
+ right: BPlusNode,
167
+ ) -> None:
168
+ parent = left.parent
169
+ if parent is None:
170
+ new_root = BPlusNode(is_leaf=False)
171
+ new_root.keys = [key]
172
+ new_root.children = [left, right]
173
+ left.parent = new_root
174
+ right.parent = new_root
175
+ self._root = new_root
176
+ return
177
+
178
+ i = parent.children.index(left) + 1
179
+ parent.keys.insert(i - 1, key)
180
+ parent.children.insert(i, right)
181
+ right.parent = parent
182
+
183
+ if len(parent.keys) > 2 * self._order - 1:
184
+ self._split_internal(parent)
185
+
186
+ def _split_internal(self, node: BPlusNode) -> None:
187
+ new_node = BPlusNode(is_leaf=False)
188
+ mid = len(node.keys) // 2
189
+ up_key = node.keys[mid]
190
+
191
+ new_node.keys = node.keys[mid + 1 :]
192
+ new_node.children = node.children[mid + 1 :]
193
+ node.keys = node.keys[:mid]
194
+ node.children = node.children[: mid + 1]
195
+
196
+ for child in new_node.children:
197
+ child.parent = new_node
198
+
199
+ self._insert_into_parent(node, up_key, new_node)
200
+
201
+ def delete(self, key: Any) -> None:
202
+ """Remove *key* from the tree.
203
+
204
+ Parameters
205
+ ----------
206
+ key:
207
+ Key to remove.
208
+
209
+ Raises
210
+ ------
211
+ KeyNotFoundError
212
+ If *key* is not present.
213
+ """
214
+ with self._lock:
215
+ leaf = self._find_leaf(self._root, key)
216
+ if key not in leaf.keys:
217
+ raise KeyNotFoundError(key)
218
+ idx = leaf.keys.index(key)
219
+ leaf.keys.pop(idx)
220
+ leaf.values.pop(idx)
221
+ self._size -= 1
222
+
223
+ def search(self, key: Any) -> Any | None:
224
+ """Return the value associated with *key*, or *None* if absent."""
225
+ with self._lock:
226
+ leaf = self._find_leaf(self._root, key)
227
+ if key in leaf.keys:
228
+ idx = leaf.keys.index(key)
229
+ return leaf.values[idx]
230
+ return None
231
+
232
+ def contains(self, key: Any) -> bool:
233
+ """Return *True* if *key* is present."""
234
+ with self._lock:
235
+ leaf = self._find_leaf(self._root, key)
236
+ return key in leaf.keys
237
+
238
+ def update(self, key: Any, value: Any) -> None:
239
+ """Update the value of an existing *key*.
240
+
241
+ Raises
242
+ ------
243
+ KeyNotFoundError
244
+ If *key* does not exist.
245
+ """
246
+ with self._lock:
247
+ leaf = self._find_leaf(self._root, key)
248
+ if key not in leaf.keys:
249
+ raise KeyNotFoundError(key)
250
+ idx = leaf.keys.index(key)
251
+ leaf.values[idx] = value
252
+
253
+ def clear(self) -> None:
254
+ """Remove all keys from the tree."""
255
+ with self._lock:
256
+ self._root = BPlusNode(is_leaf=True)
257
+ self._first_leaf = self._root
258
+ self._size = 0
259
+
260
+ # ------------------------------------------------------------------
261
+ # Metrics
262
+ # ------------------------------------------------------------------
263
+
264
+ def size(self) -> int:
265
+ """Return the number of keys currently in the tree."""
266
+ with self._lock:
267
+ return self._size
268
+
269
+ def is_empty(self) -> bool:
270
+ """Return *True* if the tree contains no keys."""
271
+ with self._lock:
272
+ return self._size == 0
273
+
274
+ def min(self) -> Any:
275
+ """Return the minimum key.
276
+
277
+ Raises
278
+ ------
279
+ EmptyTreeError
280
+ If the tree is empty.
281
+ """
282
+ with self._lock:
283
+ if self.is_empty():
284
+ raise EmptyTreeError("min")
285
+ return self._first_leaf.keys[0]
286
+
287
+ def max(self) -> Any:
288
+ """Return the maximum key.
289
+
290
+ Raises
291
+ ------
292
+ EmptyTreeError
293
+ If the tree is empty.
294
+ """
295
+ with self._lock:
296
+ if self.is_empty():
297
+ raise EmptyTreeError("max")
298
+ leaf = self._first_leaf
299
+ while leaf.next_leaf is not None:
300
+ leaf = leaf.next_leaf
301
+ return leaf.keys[-1]
302
+
303
+ # ------------------------------------------------------------------
304
+ # Leaf traversal
305
+ # ------------------------------------------------------------------
306
+
307
+ def leaf_traversal(self) -> list[Any]:
308
+ """Traverse all leaves via the leaf-linked chain.
309
+
310
+ Returns
311
+ -------
312
+ list
313
+ All keys in sorted order by walking the leaf chain.
314
+ """
315
+ with self._lock:
316
+ result: list[Any] = []
317
+ leaf = self._first_leaf
318
+ while leaf is not None:
319
+ result.extend(leaf.keys)
320
+ leaf = leaf.next_leaf
321
+ return result
322
+
323
+ # ------------------------------------------------------------------
324
+ # Range query
325
+ # ------------------------------------------------------------------
326
+
327
+ def range_query(self, lo: Any, hi: Any) -> list[Any]:
328
+ """Return all keys in [*lo*, *hi*] using the leaf chain.
329
+
330
+ Parameters
331
+ ----------
332
+ lo:
333
+ Inclusive lower bound.
334
+ hi:
335
+ Inclusive upper bound.
336
+
337
+ Returns
338
+ -------
339
+ list
340
+ Sorted keys within the range.
341
+ """
342
+ with self._lock:
343
+ result: list[Any] = []
344
+ leaf = self._find_leaf(self._root, lo)
345
+ while leaf is not None:
346
+ for k in leaf.keys:
347
+ if lo <= k <= hi:
348
+ result.append(k)
349
+ elif k > hi:
350
+ return result
351
+ leaf = leaf.next_leaf
352
+ return result
353
+
354
+ # ------------------------------------------------------------------
355
+ # Validation
356
+ # ------------------------------------------------------------------
357
+
358
+ def validate(self) -> bool:
359
+ """Verify internal B+ Tree invariants.
360
+
361
+ Checks:
362
+ 1. All leaves are at the same depth.
363
+ 2. Internal nodes have correct separator keys.
364
+ 3. Leaf chain maintains sorted order.
365
+ 4. Key counts within bounds.
366
+
367
+ Returns
368
+ -------
369
+ bool
370
+ *True* if all invariants hold.
371
+ """
372
+ with self._lock:
373
+ if self._size == 0:
374
+ return True
375
+ try:
376
+ self._validate(self._root, None, None, 0, [])
377
+ return True
378
+ except (ValueError, AssertionError, AttributeError):
379
+ return False
380
+
381
+ def _validate(
382
+ self,
383
+ node: BPlusNode | None,
384
+ lo: Any,
385
+ hi: Any,
386
+ depth: int,
387
+ leaf_depths: list[int],
388
+ ) -> None:
389
+ if node is None:
390
+ return
391
+ kc = len(node.keys)
392
+ if node is not self._root:
393
+ assert kc >= self._order - 1, f"Underflow: {kc} keys"
394
+ assert kc <= 2 * self._order - 1, f"Overflow: {kc} keys"
395
+ for i in range(kc - 1):
396
+ assert node.keys[i] < node.keys[i + 1], "Keys not sorted"
397
+ if node.is_leaf:
398
+ leaf_depths.append(depth)
399
+ if len(leaf_depths) > 1 and leaf_depths[-1] != leaf_depths[-2]:
400
+ raise ValueError("Leaves at different depths")
401
+ else:
402
+ for i, child in enumerate(node.children):
403
+ child_lo = lo if i == 0 else node.keys[i - 1]
404
+ child_hi = hi if i == kc else node.keys[i]
405
+ self._validate(child, child_lo, child_hi, depth + 1, leaf_depths)
406
+
407
+ # ------------------------------------------------------------------
408
+ # Dunder methods
409
+ # ------------------------------------------------------------------
410
+
411
+ def __len__(self) -> int:
412
+ return self.size()
413
+
414
+ def __contains__(self, key: Any) -> bool:
415
+ return self.contains(key)
416
+
417
+ def __iter__(self) -> Iterator[Any]:
418
+ with self._lock:
419
+ keys = list(self.leaf_traversal())
420
+ return iter(keys)
421
+
422
+ def __repr__(self) -> str: # pragma: no cover
423
+ with self._lock:
424
+ keys = list(self.leaf_traversal())
425
+ return f"BPlusTree(size={self._size}, order={self._order}, keys={keys})"