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.
- pkstruct/__init__.py +167 -0
- pkstruct/graphs/__init__.py +127 -0
- pkstruct/graphs/connectivity.py +157 -0
- pkstruct/graphs/directed.py +95 -0
- pkstruct/graphs/exceptions.py +63 -0
- pkstruct/graphs/graph.py +262 -0
- pkstruct/graphs/mst.py +118 -0
- pkstruct/graphs/scc.py +138 -0
- pkstruct/graphs/shortest_path.py +250 -0
- pkstruct/graphs/topo_sort.py +108 -0
- pkstruct/graphs/traversal.py +175 -0
- pkstruct/graphs/visualization.py +90 -0
- pkstruct/graphs/weighted.py +37 -0
- pkstruct/linear/__init__.py +95 -0
- pkstruct/linear/deques/__init__.py +33 -0
- pkstruct/linear/deques/deque.py +194 -0
- pkstruct/linear/deques/linked_deque.py +198 -0
- pkstruct/linear/exceptions.py +26 -0
- pkstruct/linear/linked_lists/__init__.py +5 -0
- pkstruct/linear/linked_lists/_base.py +608 -0
- pkstruct/linear/linked_lists/circular_linked_list.py +230 -0
- pkstruct/linear/linked_lists/doubly_linked_list.py +151 -0
- pkstruct/linear/linked_lists/nodes.py +68 -0
- pkstruct/linear/linked_lists/singly_linked_list.py +136 -0
- pkstruct/linear/queues/__init__.py +44 -0
- pkstruct/linear/queues/circular_queue.py +258 -0
- pkstruct/linear/queues/linked_queue.py +186 -0
- pkstruct/linear/queues/priority_queue.py +202 -0
- pkstruct/linear/queues/queue.py +174 -0
- pkstruct/linear/stacks/__init__.py +38 -0
- pkstruct/linear/stacks/array_stack.py +165 -0
- pkstruct/linear/stacks/linked_stack.py +168 -0
- pkstruct/linear/stacks/stack.py +158 -0
- pkstruct/linear/utils/__init__.py +18 -0
- pkstruct/linear/utils/benchmark.py +255 -0
- pkstruct/linear/utils/debug_tools.py +239 -0
- pkstruct/linear/utils/helpers.py +143 -0
- pkstruct/linear/utils/iterators.py +148 -0
- pkstruct/linear/visualization/__init__.py +0 -0
- pkstruct/linear/visualization/ascii_visualizer.py +114 -0
- pkstruct/linear/visualization/linked_list_visualizer.py +126 -0
- pkstruct/shared/__init__.py +67 -0
- pkstruct/shared/benchmarking/__init__.py +78 -0
- pkstruct/shared/debugging/__init__.py +69 -0
- pkstruct/shared/exceptions/__init__.py +59 -0
- pkstruct/shared/serializers/__init__.py +65 -0
- pkstruct/shared/threading/__init__.py +43 -0
- pkstruct/shared/validators/__init__.py +98 -0
- pkstruct/shared/visualization/__init__.py +21 -0
- pkstruct/trees/__init__.py +92 -0
- pkstruct/trees/avl.py +321 -0
- pkstruct/trees/balancing.py +253 -0
- pkstruct/trees/bplus.py +425 -0
- pkstruct/trees/bst.py +948 -0
- pkstruct/trees/btree.py +504 -0
- pkstruct/trees/exceptions.py +96 -0
- pkstruct/trees/fenwick_tree.py +312 -0
- pkstruct/trees/interval_tree.py +541 -0
- pkstruct/trees/node.py +356 -0
- pkstruct/trees/red_black.py +710 -0
- pkstruct/trees/segment_tree.py +398 -0
- pkstruct/trees/traversal.py +456 -0
- pkstruct/trees/tree_helpers.py +366 -0
- pkstruct/trees/utils/__init__.py +15 -0
- pkstruct/trees/utils/complexity_helpers.py +231 -0
- pkstruct/trees/visualization/__init__.py +0 -0
- pkstruct/trees/visualization/ascii_renderer.py +220 -0
- pkstruct/trees/visualization/tree_printer.py +129 -0
- pkstruct-0.1.0.dist-info/METADATA +482 -0
- pkstruct-0.1.0.dist-info/RECORD +72 -0
- pkstruct-0.1.0.dist-info/WHEEL +4 -0
- pkstruct-0.1.0.dist-info/licenses/LICENSE +21 -0
pkstruct/trees/btree.py
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pkstruct.trees.btree
|
|
3
|
+
====================
|
|
4
|
+
B-Tree implementation.
|
|
5
|
+
|
|
6
|
+
Architecture
|
|
7
|
+
------------
|
|
8
|
+
``BTree`` stores keys and values in multi-key nodes. Internal nodes have
|
|
9
|
+
children between every pair of keys. Insert triggers node splits when a
|
|
10
|
+
node overflows (exceeds ``2 * order - 1`` keys). Delete triggers merges or
|
|
11
|
+
borrowing when a node underflows (drops below ``order - 1`` keys).
|
|
12
|
+
|
|
13
|
+
All major operations run in O(log n) due to the tree's self-balancing
|
|
14
|
+
property (every leaf is at the same depth).
|
|
15
|
+
|
|
16
|
+
Public API
|
|
17
|
+
----------
|
|
18
|
+
Core CRUD
|
|
19
|
+
insert, delete, search, contains, update, clear
|
|
20
|
+
|
|
21
|
+
Metrics
|
|
22
|
+
size, is_empty, min, max
|
|
23
|
+
|
|
24
|
+
Structural
|
|
25
|
+
validate
|
|
26
|
+
|
|
27
|
+
Traversal
|
|
28
|
+
__iter__, __reversed__ (in-order)
|
|
29
|
+
|
|
30
|
+
Dunder
|
|
31
|
+
__len__, __contains__, __iter__, __repr__
|
|
32
|
+
|
|
33
|
+
Complexity
|
|
34
|
+
----------
|
|
35
|
+
Search O(log n)
|
|
36
|
+
Insert O(log n)
|
|
37
|
+
Delete O(log n)
|
|
38
|
+
Space O(n)
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
from collections.abc import Iterator
|
|
44
|
+
from typing import Any
|
|
45
|
+
|
|
46
|
+
from pkstruct.shared.threading import StructureLock
|
|
47
|
+
from pkstruct.trees.exceptions import (
|
|
48
|
+
DuplicateKeyError,
|
|
49
|
+
EmptyTreeError,
|
|
50
|
+
InvalidOrderError,
|
|
51
|
+
KeyNotFoundError,
|
|
52
|
+
)
|
|
53
|
+
from pkstruct.trees.node import BTreeNode
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class BTree:
|
|
57
|
+
"""A balanced multi-way search tree (B-Tree).
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
order:
|
|
62
|
+
Minimum degree of the tree (must be >= 2). Each node (except root)
|
|
63
|
+
must contain at least ``order - 1`` keys and at most
|
|
64
|
+
``2 * order - 1`` keys.
|
|
65
|
+
allow_duplicates:
|
|
66
|
+
When *False* (default) duplicate keys update the stored value.
|
|
67
|
+
When *True* duplicates are rejected with ``DuplicateKeyError``.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(self, order: int = 3, allow_duplicates: bool = False) -> None:
|
|
71
|
+
if order < 2:
|
|
72
|
+
raise InvalidOrderError(order)
|
|
73
|
+
self._order: int = order
|
|
74
|
+
self._allow_duplicates: bool = allow_duplicates
|
|
75
|
+
self._root: BTreeNode = BTreeNode()
|
|
76
|
+
self._size: int = 0
|
|
77
|
+
self._lock: StructureLock = StructureLock()
|
|
78
|
+
|
|
79
|
+
# ------------------------------------------------------------------
|
|
80
|
+
# Properties
|
|
81
|
+
# ------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def order(self) -> int:
|
|
85
|
+
"""Minimum degree of this B-Tree."""
|
|
86
|
+
return self._order
|
|
87
|
+
|
|
88
|
+
# ------------------------------------------------------------------
|
|
89
|
+
# Core CRUD
|
|
90
|
+
# ------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
def insert(self, key: Any, value: Any = None) -> None:
|
|
93
|
+
"""Insert *key* with optional *value*.
|
|
94
|
+
|
|
95
|
+
Parameters
|
|
96
|
+
----------
|
|
97
|
+
key:
|
|
98
|
+
Comparable key.
|
|
99
|
+
value:
|
|
100
|
+
Optional payload.
|
|
101
|
+
|
|
102
|
+
Raises
|
|
103
|
+
------
|
|
104
|
+
DuplicateKeyError
|
|
105
|
+
If ``allow_duplicates=True`` and *key* already exists.
|
|
106
|
+
"""
|
|
107
|
+
with self._lock:
|
|
108
|
+
# Check for duplicate
|
|
109
|
+
try:
|
|
110
|
+
existing = self._find(self._root, key)
|
|
111
|
+
if existing is not None:
|
|
112
|
+
if self._allow_duplicates:
|
|
113
|
+
raise DuplicateKeyError(key)
|
|
114
|
+
# Update value in-place
|
|
115
|
+
self._update_value(self._root, key, value)
|
|
116
|
+
return
|
|
117
|
+
except KeyNotFoundError:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
root = self._root
|
|
121
|
+
if len(root.keys) == 2 * self._order - 1:
|
|
122
|
+
new_root = BTreeNode()
|
|
123
|
+
new_root.children.append(root)
|
|
124
|
+
root.parent = new_root
|
|
125
|
+
self._split_child(new_root, 0)
|
|
126
|
+
self._root = new_root
|
|
127
|
+
self._insert_non_full(self._root, key, value)
|
|
128
|
+
self._size += 1
|
|
129
|
+
|
|
130
|
+
def _insert_non_full(self, node: BTreeNode, key: Any, value: Any) -> None:
|
|
131
|
+
i = len(node.keys) - 1
|
|
132
|
+
if node.is_leaf():
|
|
133
|
+
node.keys.append(None)
|
|
134
|
+
node.values.append(None)
|
|
135
|
+
while i >= 0 and key < node.keys[i]:
|
|
136
|
+
node.keys[i + 1] = node.keys[i]
|
|
137
|
+
node.values[i + 1] = node.values[i]
|
|
138
|
+
i -= 1
|
|
139
|
+
node.keys[i + 1] = key
|
|
140
|
+
node.values[i + 1] = value
|
|
141
|
+
else:
|
|
142
|
+
while i >= 0 and key < node.keys[i]:
|
|
143
|
+
i -= 1
|
|
144
|
+
i += 1
|
|
145
|
+
if len(node.children[i].keys) == 2 * self._order - 1:
|
|
146
|
+
self._split_child(node, i)
|
|
147
|
+
if key > node.keys[i]:
|
|
148
|
+
i += 1
|
|
149
|
+
self._insert_non_full(node.children[i], key, value)
|
|
150
|
+
|
|
151
|
+
def _split_child(self, parent: BTreeNode, index: int) -> None:
|
|
152
|
+
order = self._order
|
|
153
|
+
child = parent.children[index]
|
|
154
|
+
new_child = BTreeNode()
|
|
155
|
+
new_child.parent = parent
|
|
156
|
+
mid = order - 1
|
|
157
|
+
|
|
158
|
+
# Save the key/value to promote before truncating
|
|
159
|
+
mid_key = child.keys[mid]
|
|
160
|
+
mid_value = child.values[mid]
|
|
161
|
+
|
|
162
|
+
# Copy second half of keys/values
|
|
163
|
+
new_child.keys = child.keys[mid + 1 :]
|
|
164
|
+
new_child.values = child.values[mid + 1 :]
|
|
165
|
+
child.keys = child.keys[:mid]
|
|
166
|
+
child.values = child.values[:mid]
|
|
167
|
+
|
|
168
|
+
if not child.is_leaf():
|
|
169
|
+
new_child.children = child.children[mid + 1 :]
|
|
170
|
+
child.children = child.children[: mid + 1]
|
|
171
|
+
|
|
172
|
+
# Promote middle key to parent
|
|
173
|
+
parent.keys.insert(index, mid_key)
|
|
174
|
+
parent.values.insert(index, mid_value)
|
|
175
|
+
parent.children.insert(index + 1, new_child)
|
|
176
|
+
|
|
177
|
+
def delete(self, key: Any) -> None:
|
|
178
|
+
"""Remove *key* from the tree.
|
|
179
|
+
|
|
180
|
+
Parameters
|
|
181
|
+
----------
|
|
182
|
+
key:
|
|
183
|
+
Key to remove.
|
|
184
|
+
|
|
185
|
+
Raises
|
|
186
|
+
------
|
|
187
|
+
KeyNotFoundError
|
|
188
|
+
If *key* is not present.
|
|
189
|
+
"""
|
|
190
|
+
with self._lock:
|
|
191
|
+
if not self._find(self._root, key):
|
|
192
|
+
raise KeyNotFoundError(key)
|
|
193
|
+
self._delete(self._root, key)
|
|
194
|
+
self._size -= 1
|
|
195
|
+
if len(self._root.keys) == 0 and not self._root.is_leaf():
|
|
196
|
+
self._root = self._root.children[0]
|
|
197
|
+
self._root.parent = None
|
|
198
|
+
|
|
199
|
+
def _delete(self, node: BTreeNode, key: Any) -> None:
|
|
200
|
+
if node.is_leaf():
|
|
201
|
+
if key in node.keys:
|
|
202
|
+
idx = node.keys.index(key)
|
|
203
|
+
node.keys.pop(idx)
|
|
204
|
+
node.values.pop(idx)
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
if key in node.keys:
|
|
208
|
+
idx = node.keys.index(key)
|
|
209
|
+
if len(node.children[idx].keys) >= self._order:
|
|
210
|
+
pred = self._get_predecessor(node.children[idx])
|
|
211
|
+
node.keys[idx], node.values[idx] = pred
|
|
212
|
+
self._delete(node.children[idx], pred[0])
|
|
213
|
+
elif len(node.children[idx + 1].keys) >= self._order:
|
|
214
|
+
succ = self._get_successor(node.children[idx + 1])
|
|
215
|
+
node.keys[idx], node.values[idx] = succ
|
|
216
|
+
self._delete(node.children[idx + 1], succ[0])
|
|
217
|
+
else:
|
|
218
|
+
self._merge(node, idx)
|
|
219
|
+
self._delete(node.children[idx], key)
|
|
220
|
+
else:
|
|
221
|
+
idx = 0
|
|
222
|
+
while idx < len(node.keys) and key > node.keys[idx]:
|
|
223
|
+
idx += 1
|
|
224
|
+
if len(node.children[idx].keys) < self._order:
|
|
225
|
+
self._fix_child(node, idx)
|
|
226
|
+
if idx > 0 and node.children[idx - 1] is self._get_sibling(node, idx):
|
|
227
|
+
idx -= 1
|
|
228
|
+
self._delete(node.children[idx], key)
|
|
229
|
+
|
|
230
|
+
def _get_predecessor(self, node: BTreeNode) -> tuple[Any, Any]:
|
|
231
|
+
while not node.is_leaf():
|
|
232
|
+
node = node.children[-1]
|
|
233
|
+
return (node.keys[-1], node.values[-1])
|
|
234
|
+
|
|
235
|
+
def _get_successor(self, node: BTreeNode) -> tuple[Any, Any]:
|
|
236
|
+
while not node.is_leaf():
|
|
237
|
+
node = node.children[0]
|
|
238
|
+
return (node.keys[0], node.values[0])
|
|
239
|
+
|
|
240
|
+
def _merge(self, parent: BTreeNode, idx: int) -> None:
|
|
241
|
+
left = parent.children[idx]
|
|
242
|
+
right = parent.children[idx + 1]
|
|
243
|
+
left.keys.append(parent.keys.pop(idx))
|
|
244
|
+
left.values.append(parent.values.pop(idx))
|
|
245
|
+
left.keys.extend(right.keys)
|
|
246
|
+
left.values.extend(right.values)
|
|
247
|
+
if not right.is_leaf():
|
|
248
|
+
left.children.extend(right.children)
|
|
249
|
+
parent.children.pop(idx + 1)
|
|
250
|
+
|
|
251
|
+
def _fix_child(self, parent: BTreeNode, idx: int) -> None:
|
|
252
|
+
if idx > 0 and len(parent.children[idx - 1].keys) >= self._order:
|
|
253
|
+
self._borrow_from_left(parent, idx)
|
|
254
|
+
elif idx < len(parent.children) - 1 and len(parent.children[idx + 1].keys) >= self._order:
|
|
255
|
+
self._borrow_from_right(parent, idx)
|
|
256
|
+
else:
|
|
257
|
+
if idx < len(parent.children) - 1:
|
|
258
|
+
self._merge(parent, idx)
|
|
259
|
+
else:
|
|
260
|
+
self._merge(parent, idx - 1)
|
|
261
|
+
|
|
262
|
+
def _borrow_from_left(self, parent: BTreeNode, idx: int) -> None:
|
|
263
|
+
child = parent.children[idx]
|
|
264
|
+
left = parent.children[idx - 1]
|
|
265
|
+
child.keys.insert(0, parent.keys[idx - 1])
|
|
266
|
+
child.values.insert(0, parent.values[idx - 1])
|
|
267
|
+
parent.keys[idx - 1] = left.keys.pop()
|
|
268
|
+
parent.values[idx - 1] = left.values.pop()
|
|
269
|
+
if not left.is_leaf():
|
|
270
|
+
child.children.insert(0, left.children.pop())
|
|
271
|
+
|
|
272
|
+
def _borrow_from_right(self, parent: BTreeNode, idx: int) -> None:
|
|
273
|
+
child = parent.children[idx]
|
|
274
|
+
right = parent.children[idx + 1]
|
|
275
|
+
child.keys.append(parent.keys[idx])
|
|
276
|
+
child.values.append(parent.values[idx])
|
|
277
|
+
parent.keys[idx] = right.keys.pop(0)
|
|
278
|
+
parent.values[idx] = right.values.pop(0)
|
|
279
|
+
if not right.is_leaf():
|
|
280
|
+
child.children.append(right.children.pop(0))
|
|
281
|
+
|
|
282
|
+
def _get_sibling(self, node: BTreeNode, idx: int) -> BTreeNode:
|
|
283
|
+
if idx < len(node.children) - 1:
|
|
284
|
+
return node.children[idx + 1]
|
|
285
|
+
return node.children[idx - 1]
|
|
286
|
+
|
|
287
|
+
def search(self, key: Any) -> Any | None:
|
|
288
|
+
"""Return the value associated with *key*, or *None* if absent."""
|
|
289
|
+
with self._lock:
|
|
290
|
+
try:
|
|
291
|
+
node, idx = self._search(self._root, key)
|
|
292
|
+
return node.values[idx]
|
|
293
|
+
except (KeyNotFoundError, TypeError):
|
|
294
|
+
return None
|
|
295
|
+
|
|
296
|
+
def _search(self, node: BTreeNode | None, key: Any) -> tuple[BTreeNode, int]:
|
|
297
|
+
if node is None:
|
|
298
|
+
raise KeyNotFoundError(key)
|
|
299
|
+
i = 0
|
|
300
|
+
while i < len(node.keys) and key > node.keys[i]:
|
|
301
|
+
i += 1
|
|
302
|
+
if i < len(node.keys) and key == node.keys[i]:
|
|
303
|
+
return node, i
|
|
304
|
+
if node.is_leaf():
|
|
305
|
+
raise KeyNotFoundError(key)
|
|
306
|
+
return self._search(node.children[i], key)
|
|
307
|
+
|
|
308
|
+
def contains(self, key: Any) -> bool:
|
|
309
|
+
"""Return *True* if *key* is present."""
|
|
310
|
+
with self._lock:
|
|
311
|
+
try:
|
|
312
|
+
self._search(self._root, key)
|
|
313
|
+
return True
|
|
314
|
+
except KeyNotFoundError:
|
|
315
|
+
return False
|
|
316
|
+
|
|
317
|
+
def update(self, key: Any, value: Any) -> None:
|
|
318
|
+
"""Update the value of an existing *key*.
|
|
319
|
+
|
|
320
|
+
Raises
|
|
321
|
+
------
|
|
322
|
+
KeyNotFoundError
|
|
323
|
+
If *key* does not exist.
|
|
324
|
+
"""
|
|
325
|
+
with self._lock:
|
|
326
|
+
node, idx = self._search(self._root, key)
|
|
327
|
+
node.values[idx] = value
|
|
328
|
+
|
|
329
|
+
def _update_value(self, node: BTreeNode, key: Any, value: Any) -> None:
|
|
330
|
+
try:
|
|
331
|
+
n, idx = self._search(node, key)
|
|
332
|
+
n.values[idx] = value
|
|
333
|
+
except KeyNotFoundError:
|
|
334
|
+
pass
|
|
335
|
+
|
|
336
|
+
def _find(self, node: BTreeNode | None, key: Any) -> BTreeNode | None:
|
|
337
|
+
try:
|
|
338
|
+
n, _ = self._search(node, key)
|
|
339
|
+
return n
|
|
340
|
+
except KeyNotFoundError:
|
|
341
|
+
return None
|
|
342
|
+
|
|
343
|
+
def clear(self) -> None:
|
|
344
|
+
"""Remove all keys from the tree."""
|
|
345
|
+
with self._lock:
|
|
346
|
+
self._root = BTreeNode()
|
|
347
|
+
self._size = 0
|
|
348
|
+
|
|
349
|
+
# ------------------------------------------------------------------
|
|
350
|
+
# Metrics
|
|
351
|
+
# ------------------------------------------------------------------
|
|
352
|
+
|
|
353
|
+
def size(self) -> int:
|
|
354
|
+
"""Return the number of keys currently in the tree."""
|
|
355
|
+
with self._lock:
|
|
356
|
+
return self._size
|
|
357
|
+
|
|
358
|
+
def is_empty(self) -> bool:
|
|
359
|
+
"""Return *True* if the tree contains no keys."""
|
|
360
|
+
with self._lock:
|
|
361
|
+
return self._size == 0
|
|
362
|
+
|
|
363
|
+
def min(self) -> Any:
|
|
364
|
+
"""Return the minimum key.
|
|
365
|
+
|
|
366
|
+
Raises
|
|
367
|
+
------
|
|
368
|
+
EmptyTreeError
|
|
369
|
+
If the tree is empty.
|
|
370
|
+
"""
|
|
371
|
+
with self._lock:
|
|
372
|
+
if self.is_empty():
|
|
373
|
+
raise EmptyTreeError("min")
|
|
374
|
+
node = self._root
|
|
375
|
+
while not node.is_leaf():
|
|
376
|
+
node = node.children[0]
|
|
377
|
+
return node.keys[0]
|
|
378
|
+
|
|
379
|
+
def max(self) -> Any:
|
|
380
|
+
"""Return the maximum key.
|
|
381
|
+
|
|
382
|
+
Raises
|
|
383
|
+
------
|
|
384
|
+
EmptyTreeError
|
|
385
|
+
If the tree is empty.
|
|
386
|
+
"""
|
|
387
|
+
with self._lock:
|
|
388
|
+
if self.is_empty():
|
|
389
|
+
raise EmptyTreeError("max")
|
|
390
|
+
node = self._root
|
|
391
|
+
while not node.is_leaf():
|
|
392
|
+
node = node.children[-1]
|
|
393
|
+
return node.keys[-1]
|
|
394
|
+
|
|
395
|
+
# ------------------------------------------------------------------
|
|
396
|
+
# Validation
|
|
397
|
+
# ------------------------------------------------------------------
|
|
398
|
+
|
|
399
|
+
def validate(self) -> bool:
|
|
400
|
+
"""Verify internal B-Tree invariants.
|
|
401
|
+
|
|
402
|
+
Checks:
|
|
403
|
+
1. Every node (except root) has at least ``order - 1`` keys.
|
|
404
|
+
2. Every node has at most ``2 * order - 1`` keys.
|
|
405
|
+
3. All leaves are at the same depth.
|
|
406
|
+
4. Keys within each node are sorted.
|
|
407
|
+
5. BST ordering is maintained across children.
|
|
408
|
+
|
|
409
|
+
Returns
|
|
410
|
+
-------
|
|
411
|
+
bool
|
|
412
|
+
*True* if all invariants hold.
|
|
413
|
+
"""
|
|
414
|
+
with self._lock:
|
|
415
|
+
if self._size == 0:
|
|
416
|
+
return True
|
|
417
|
+
try:
|
|
418
|
+
self._validate(self._root, None, None)
|
|
419
|
+
return True
|
|
420
|
+
except (ValueError, AssertionError):
|
|
421
|
+
return False
|
|
422
|
+
|
|
423
|
+
def _validate(
|
|
424
|
+
self,
|
|
425
|
+
node: BTreeNode | None,
|
|
426
|
+
lo: Any,
|
|
427
|
+
hi: Any,
|
|
428
|
+
depth: int | None = None,
|
|
429
|
+
leaf_depth: list | None = None,
|
|
430
|
+
) -> None:
|
|
431
|
+
if node is None:
|
|
432
|
+
return
|
|
433
|
+
if leaf_depth is None:
|
|
434
|
+
leaf_depth = []
|
|
435
|
+
if depth is None:
|
|
436
|
+
depth = 0
|
|
437
|
+
|
|
438
|
+
# Key count invariants
|
|
439
|
+
kc = len(node.keys)
|
|
440
|
+
assert kc >= self._order - 1 or node.parent is None, (
|
|
441
|
+
f"Underflow: node has {kc} keys, need at least {self._order - 1}"
|
|
442
|
+
)
|
|
443
|
+
assert kc <= 2 * self._order - 1, (
|
|
444
|
+
f"Overflow: node has {kc} keys, max is {2 * self._order - 1}"
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
# Keys sorted
|
|
448
|
+
for i in range(kc - 1):
|
|
449
|
+
assert node.keys[i] < node.keys[i + 1], "Keys not sorted"
|
|
450
|
+
|
|
451
|
+
# BST ordering
|
|
452
|
+
for _, k in enumerate(node.keys):
|
|
453
|
+
if lo is not None and k <= lo:
|
|
454
|
+
raise ValueError(f"Key {k} <= lower bound {lo}")
|
|
455
|
+
if hi is not None and k >= hi:
|
|
456
|
+
raise ValueError(f"Key {k} >= upper bound {hi}")
|
|
457
|
+
|
|
458
|
+
if node.is_leaf():
|
|
459
|
+
leaf_depth.append(depth)
|
|
460
|
+
if len(leaf_depth) > 1 and leaf_depth[-1] != leaf_depth[-2]:
|
|
461
|
+
raise ValueError("Leaves at different depths")
|
|
462
|
+
return
|
|
463
|
+
|
|
464
|
+
# Children
|
|
465
|
+
assert len(node.children) == kc + 1, "Wrong child count"
|
|
466
|
+
|
|
467
|
+
for i, child in enumerate(node.children):
|
|
468
|
+
child_lo = lo if i == 0 else node.keys[i - 1]
|
|
469
|
+
child_hi = hi if i == kc else node.keys[i]
|
|
470
|
+
self._validate(child, child_lo, child_hi, depth + 1, leaf_depth)
|
|
471
|
+
|
|
472
|
+
# ------------------------------------------------------------------
|
|
473
|
+
# Dunder methods
|
|
474
|
+
# ------------------------------------------------------------------
|
|
475
|
+
|
|
476
|
+
def __len__(self) -> int:
|
|
477
|
+
"""Return the number of keys in the tree."""
|
|
478
|
+
return self.size()
|
|
479
|
+
|
|
480
|
+
def __contains__(self, key: Any) -> bool:
|
|
481
|
+
"""Support ``key in tree`` syntax."""
|
|
482
|
+
return self.contains(key)
|
|
483
|
+
|
|
484
|
+
def __iter__(self) -> Iterator[Any]:
|
|
485
|
+
"""Iterate over keys in ascending order."""
|
|
486
|
+
with self._lock:
|
|
487
|
+
keys = list(self._inorder(self._root))
|
|
488
|
+
return iter(keys)
|
|
489
|
+
|
|
490
|
+
def _inorder(self, node: BTreeNode | None) -> Iterator[Any]:
|
|
491
|
+
if node is None:
|
|
492
|
+
return
|
|
493
|
+
if node.is_leaf():
|
|
494
|
+
yield from node.keys
|
|
495
|
+
return
|
|
496
|
+
for i in range(len(node.keys)):
|
|
497
|
+
yield from self._inorder(node.children[i])
|
|
498
|
+
yield node.keys[i]
|
|
499
|
+
yield from self._inorder(node.children[-1])
|
|
500
|
+
|
|
501
|
+
def __repr__(self) -> str: # pragma: no cover
|
|
502
|
+
with self._lock:
|
|
503
|
+
keys = list(self._inorder(self._root))
|
|
504
|
+
return f"BTree(size={self._size}, order={self._order}, keys={keys})"
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pkstruct.trees.exceptions
|
|
3
|
+
=========================
|
|
4
|
+
Custom exception hierarchy for the trees module.
|
|
5
|
+
|
|
6
|
+
All tree-specific exceptions inherit from :class:`TreeError` so callers
|
|
7
|
+
can catch the entire family with a single ``except TreeError`` clause
|
|
8
|
+
while still being able to distinguish individual error types.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"TreeError",
|
|
15
|
+
"KeyNotFoundError",
|
|
16
|
+
"DuplicateKeyError",
|
|
17
|
+
"EmptyTreeError",
|
|
18
|
+
"InvalidOrderError",
|
|
19
|
+
"InvalidOperationError",
|
|
20
|
+
"TreeBalanceError",
|
|
21
|
+
"SerializationError",
|
|
22
|
+
"InvalidIntervalError",
|
|
23
|
+
"IndexOutOfBoundsError",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TreeError(Exception):
|
|
28
|
+
"""Base exception for all tree-related errors."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class KeyNotFoundError(TreeError):
|
|
32
|
+
"""Raised when a requested key does not exist in the tree."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, key: object) -> None:
|
|
35
|
+
super().__init__(f"Key not found: {key!r}")
|
|
36
|
+
self.key = key
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class DuplicateKeyError(TreeError):
|
|
40
|
+
"""Raised when inserting a key that already exists (where disallowed)."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, key: object) -> None:
|
|
43
|
+
super().__init__(f"Duplicate key: {key!r}")
|
|
44
|
+
self.key = key
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class EmptyTreeError(TreeError):
|
|
48
|
+
"""Raised when an operation requires a non-empty tree."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, operation: str = "operation") -> None:
|
|
51
|
+
super().__init__(f"Cannot perform '{operation}' on an empty tree.")
|
|
52
|
+
self.operation = operation
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class InvalidOrderError(TreeError):
|
|
56
|
+
"""Raised when a B-Tree / B+Tree order value is invalid."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, order: int) -> None:
|
|
59
|
+
super().__init__(f"Tree order must be >= 2, got {order}.")
|
|
60
|
+
self.order = order
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class InvalidOperationError(TreeError):
|
|
64
|
+
"""Raised when an unsupported operation string is requested."""
|
|
65
|
+
|
|
66
|
+
def __init__(self, operation: str) -> None:
|
|
67
|
+
super().__init__(f"Unsupported operation: {operation!r}.")
|
|
68
|
+
self.operation = operation
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class TreeBalanceError(TreeError):
|
|
72
|
+
"""Raised when an internal balance invariant is violated."""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SerializationError(TreeError):
|
|
76
|
+
"""Raised when tree (de)serialization fails."""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class InvalidIntervalError(TreeError):
|
|
80
|
+
"""Raised when an interval [lo, hi] has lo > hi."""
|
|
81
|
+
|
|
82
|
+
def __init__(self, lo: object, hi: object) -> None:
|
|
83
|
+
super().__init__(f"Invalid interval: [{lo!r}, {hi!r}] (lo > hi).")
|
|
84
|
+
self.lo = lo
|
|
85
|
+
self.hi = hi
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class IndexOutOfBoundsError(TreeError):
|
|
89
|
+
"""Raised when a k-th order statistic index is out of range."""
|
|
90
|
+
|
|
91
|
+
def __init__(self, index: int, size: int) -> None:
|
|
92
|
+
super().__init__(
|
|
93
|
+
f"Index {index} out of bounds for tree of size {size}."
|
|
94
|
+
)
|
|
95
|
+
self.index = index
|
|
96
|
+
self.size = size
|