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
|
@@ -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)
|
pkstruct/trees/bplus.py
ADDED
|
@@ -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})"
|