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,710 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pkstruct.trees.red_black
|
|
3
|
+
========================
|
|
4
|
+
Production-grade Red-Black Tree.
|
|
5
|
+
|
|
6
|
+
Architecture
|
|
7
|
+
------------
|
|
8
|
+
``RedBlackTree`` is a standalone class (not a subclass of BST) because the
|
|
9
|
+
RB delete fixup relies on a sentinel NIL node that would conflict with the
|
|
10
|
+
plain ``TreeNode`` infrastructure used by ``BinarySearchTree``.
|
|
11
|
+
|
|
12
|
+
Shared infrastructure used
|
|
13
|
+
--------------------------
|
|
14
|
+
* ``RBNode`` from ``node.py`` – carries ``color``, ``parent``,
|
|
15
|
+
``left``, ``right`` attributes.
|
|
16
|
+
* ``rotate`` from ``balancing.py`` – generic left/right rotation;
|
|
17
|
+
called as ``rotate(node, "left")`` / ``rotate(node, "right")``.
|
|
18
|
+
|
|
19
|
+
Constants
|
|
20
|
+
---------
|
|
21
|
+
RED = 0
|
|
22
|
+
BLACK = 1
|
|
23
|
+
|
|
24
|
+
Red-Black invariants maintained
|
|
25
|
+
---------------------------------
|
|
26
|
+
1. Every node is RED or BLACK.
|
|
27
|
+
2. The root is always BLACK.
|
|
28
|
+
3. No RED node has a RED parent (no two consecutive red nodes).
|
|
29
|
+
4. Every path from any node to its descendant NIL leaves passes through
|
|
30
|
+
the same number of BLACK nodes (equal black-height).
|
|
31
|
+
5. BST ordering: left.key < node.key < right.key.
|
|
32
|
+
|
|
33
|
+
Public API
|
|
34
|
+
----------
|
|
35
|
+
Core CRUD
|
|
36
|
+
insert, delete, search, contains, update, clear
|
|
37
|
+
|
|
38
|
+
Metrics
|
|
39
|
+
size, height, is_empty, min, max, black_height
|
|
40
|
+
|
|
41
|
+
Structural
|
|
42
|
+
validate, is_red_black_valid, fix_insert, fix_delete, copy
|
|
43
|
+
|
|
44
|
+
Traversal / navigation
|
|
45
|
+
predecessor, successor
|
|
46
|
+
|
|
47
|
+
Dunder
|
|
48
|
+
__len__, __contains__, __iter__, __repr__
|
|
49
|
+
|
|
50
|
+
Complexity
|
|
51
|
+
----------
|
|
52
|
+
All major operations are O(log n) due to the RB height bound
|
|
53
|
+
h ≤ 2 · log2(n + 1).
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
from __future__ import annotations
|
|
57
|
+
|
|
58
|
+
from collections.abc import Generator, Iterator
|
|
59
|
+
from typing import Any
|
|
60
|
+
|
|
61
|
+
from pkstruct.shared.threading import StructureLock
|
|
62
|
+
from pkstruct.trees.balancing import rotate
|
|
63
|
+
from pkstruct.trees.node import RBNode
|
|
64
|
+
|
|
65
|
+
RED: int = 0
|
|
66
|
+
BLACK: int = 1
|
|
67
|
+
|
|
68
|
+
# Sentinel NIL node – shared across the entire module, never modified.
|
|
69
|
+
_NIL: RBNode = RBNode(key=None, value=None)
|
|
70
|
+
_NIL.color = BLACK
|
|
71
|
+
_NIL.left = None # type: ignore[assignment]
|
|
72
|
+
_NIL.right = None # type: ignore[assignment]
|
|
73
|
+
_NIL.parent = None # type: ignore[assignment]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _is_nil(node: RBNode | None) -> bool:
|
|
77
|
+
"""Return *True* if *node* is the sentinel NIL or Python *None*."""
|
|
78
|
+
return node is None or node is _NIL
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class RedBlackTree:
|
|
82
|
+
"""Self-balancing Red-Black Tree.
|
|
83
|
+
|
|
84
|
+
All major operations run in O(log n) time. The four Red-Black
|
|
85
|
+
invariants are enforced automatically on every insert and delete.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
allow_duplicates:
|
|
90
|
+
When *False* (default) duplicate keys update the stored value.
|
|
91
|
+
When *True* duplicates raise ``ValueError``.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
# ------------------------------------------------------------------
|
|
95
|
+
# Construction
|
|
96
|
+
# ------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
def __init__(self, allow_duplicates: bool = False) -> None:
|
|
99
|
+
self._root: RBNode = _NIL
|
|
100
|
+
self._size: int = 0
|
|
101
|
+
self._allow_duplicates = allow_duplicates
|
|
102
|
+
self._lock: StructureLock = StructureLock()
|
|
103
|
+
|
|
104
|
+
# ------------------------------------------------------------------
|
|
105
|
+
# Core CRUD
|
|
106
|
+
# ------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
def insert(self, key: Any, value: Any = None) -> None:
|
|
109
|
+
"""Insert *key* with optional *value*, fixing RB invariants afterwards.
|
|
110
|
+
|
|
111
|
+
Parameters
|
|
112
|
+
----------
|
|
113
|
+
key:
|
|
114
|
+
Comparable key used for BST ordering.
|
|
115
|
+
value:
|
|
116
|
+
Arbitrary payload stored alongside the key.
|
|
117
|
+
|
|
118
|
+
Raises
|
|
119
|
+
------
|
|
120
|
+
ValueError
|
|
121
|
+
If ``allow_duplicates=True`` and *key* already exists.
|
|
122
|
+
"""
|
|
123
|
+
with self._lock:
|
|
124
|
+
existing = self._find(self._root, key)
|
|
125
|
+
if not _is_nil(existing):
|
|
126
|
+
if self._allow_duplicates:
|
|
127
|
+
raise ValueError(f"Duplicate key: {key!r}")
|
|
128
|
+
existing.value = value
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
new_node = RBNode(key, value)
|
|
132
|
+
new_node.color = RED
|
|
133
|
+
new_node.left = _NIL
|
|
134
|
+
new_node.right = _NIL
|
|
135
|
+
new_node.parent = _NIL
|
|
136
|
+
|
|
137
|
+
# Standard BST insert
|
|
138
|
+
parent: RBNode = _NIL
|
|
139
|
+
current: RBNode = self._root
|
|
140
|
+
while not _is_nil(current):
|
|
141
|
+
parent = current
|
|
142
|
+
current = current.left if key < current.key else current.right
|
|
143
|
+
|
|
144
|
+
new_node.parent = parent
|
|
145
|
+
if _is_nil(parent):
|
|
146
|
+
self._root = new_node
|
|
147
|
+
elif key < parent.key:
|
|
148
|
+
parent.left = new_node
|
|
149
|
+
else:
|
|
150
|
+
parent.right = new_node
|
|
151
|
+
|
|
152
|
+
self._size += 1
|
|
153
|
+
self.fix_insert(new_node)
|
|
154
|
+
|
|
155
|
+
def fix_insert(self, node: RBNode) -> None:
|
|
156
|
+
"""Restore Red-Black properties after a standard BST insert.
|
|
157
|
+
|
|
158
|
+
Handles the three canonical cases:
|
|
159
|
+
- Uncle is RED → recolor.
|
|
160
|
+
- Uncle is BLACK, triangle → rotate to line case.
|
|
161
|
+
- Uncle is BLACK, line → rotate and recolor.
|
|
162
|
+
|
|
163
|
+
Parameters
|
|
164
|
+
----------
|
|
165
|
+
node:
|
|
166
|
+
The newly inserted RED node.
|
|
167
|
+
"""
|
|
168
|
+
while not _is_nil(node.parent) and node.parent.color == RED:
|
|
169
|
+
parent = node.parent
|
|
170
|
+
grandparent = parent.parent
|
|
171
|
+
if _is_nil(grandparent):
|
|
172
|
+
break
|
|
173
|
+
|
|
174
|
+
if parent is grandparent.left:
|
|
175
|
+
uncle = grandparent.right
|
|
176
|
+
if not _is_nil(uncle) and uncle.color == RED:
|
|
177
|
+
# Case 1: recolor
|
|
178
|
+
parent.color = BLACK
|
|
179
|
+
uncle.color = BLACK
|
|
180
|
+
grandparent.color = RED
|
|
181
|
+
node = grandparent
|
|
182
|
+
else:
|
|
183
|
+
if node is parent.right:
|
|
184
|
+
# Case 2: triangle → convert to line
|
|
185
|
+
node = parent
|
|
186
|
+
new_root = rotate(node, "left", self._root)
|
|
187
|
+
if _is_nil(new_root.parent):
|
|
188
|
+
self._root = new_root
|
|
189
|
+
parent = node.parent
|
|
190
|
+
grandparent = parent.parent if not _is_nil(parent) else _NIL
|
|
191
|
+
# Case 3: line
|
|
192
|
+
if not _is_nil(parent):
|
|
193
|
+
parent.color = BLACK
|
|
194
|
+
if not _is_nil(grandparent):
|
|
195
|
+
grandparent.color = RED
|
|
196
|
+
new_root = rotate(grandparent, "right", self._root)
|
|
197
|
+
if _is_nil(new_root.parent):
|
|
198
|
+
self._root = new_root
|
|
199
|
+
else:
|
|
200
|
+
uncle = grandparent.left
|
|
201
|
+
if not _is_nil(uncle) and uncle.color == RED:
|
|
202
|
+
# Case 1 (mirror)
|
|
203
|
+
parent.color = BLACK
|
|
204
|
+
uncle.color = BLACK
|
|
205
|
+
grandparent.color = RED
|
|
206
|
+
node = grandparent
|
|
207
|
+
else:
|
|
208
|
+
if node is parent.left:
|
|
209
|
+
# Case 2 (mirror)
|
|
210
|
+
node = parent
|
|
211
|
+
new_root = rotate(node, "right", self._root)
|
|
212
|
+
if _is_nil(new_root.parent):
|
|
213
|
+
self._root = new_root
|
|
214
|
+
parent = node.parent
|
|
215
|
+
grandparent = parent.parent if not _is_nil(parent) else _NIL
|
|
216
|
+
# Case 3 (mirror)
|
|
217
|
+
if not _is_nil(parent):
|
|
218
|
+
parent.color = BLACK
|
|
219
|
+
if not _is_nil(grandparent):
|
|
220
|
+
grandparent.color = RED
|
|
221
|
+
new_root = rotate(grandparent, "left", self._root)
|
|
222
|
+
if _is_nil(new_root.parent):
|
|
223
|
+
self._root = new_root
|
|
224
|
+
|
|
225
|
+
self._root.color = BLACK
|
|
226
|
+
|
|
227
|
+
def delete(self, key: Any) -> None:
|
|
228
|
+
"""Remove *key* from the tree, fixing RB invariants afterwards.
|
|
229
|
+
|
|
230
|
+
Parameters
|
|
231
|
+
----------
|
|
232
|
+
key:
|
|
233
|
+
Key to delete.
|
|
234
|
+
|
|
235
|
+
Raises
|
|
236
|
+
------
|
|
237
|
+
KeyError
|
|
238
|
+
If *key* is not present.
|
|
239
|
+
"""
|
|
240
|
+
with self._lock:
|
|
241
|
+
target = self._find(self._root, key)
|
|
242
|
+
if _is_nil(target):
|
|
243
|
+
raise KeyError(key)
|
|
244
|
+
self._rb_delete(target)
|
|
245
|
+
self._size -= 1
|
|
246
|
+
|
|
247
|
+
def _rb_delete(self, z: RBNode) -> None:
|
|
248
|
+
"""Standard Cormen RB-delete on node *z*."""
|
|
249
|
+
y = z
|
|
250
|
+
y_original_color = y.color
|
|
251
|
+
x: RBNode
|
|
252
|
+
|
|
253
|
+
if _is_nil(z.left):
|
|
254
|
+
x = z.right
|
|
255
|
+
self._transplant(z, z.right)
|
|
256
|
+
elif _is_nil(z.right):
|
|
257
|
+
x = z.left
|
|
258
|
+
self._transplant(z, z.left)
|
|
259
|
+
else:
|
|
260
|
+
# y = minimum of z.right
|
|
261
|
+
y = self._min_node(z.right)
|
|
262
|
+
y_original_color = y.color
|
|
263
|
+
x = y.right
|
|
264
|
+
if y.parent is z:
|
|
265
|
+
x.parent = y
|
|
266
|
+
else:
|
|
267
|
+
self._transplant(y, y.right)
|
|
268
|
+
y.right = z.right
|
|
269
|
+
y.right.parent = y
|
|
270
|
+
self._transplant(z, y)
|
|
271
|
+
y.left = z.left
|
|
272
|
+
y.left.parent = y
|
|
273
|
+
y.color = z.color
|
|
274
|
+
|
|
275
|
+
if y_original_color == BLACK:
|
|
276
|
+
self.fix_delete(x)
|
|
277
|
+
|
|
278
|
+
def _transplant(self, u: RBNode, v: RBNode) -> None:
|
|
279
|
+
"""Replace subtree rooted at *u* with subtree rooted at *v*."""
|
|
280
|
+
if _is_nil(u.parent):
|
|
281
|
+
self._root = v
|
|
282
|
+
elif u is u.parent.left:
|
|
283
|
+
u.parent.left = v
|
|
284
|
+
else:
|
|
285
|
+
u.parent.right = v
|
|
286
|
+
v.parent = u.parent
|
|
287
|
+
|
|
288
|
+
def fix_delete(self, x: RBNode) -> None:
|
|
289
|
+
"""Restore Red-Black properties after a standard BST delete.
|
|
290
|
+
|
|
291
|
+
Handles the four canonical cases for a doubly-black node *x*.
|
|
292
|
+
|
|
293
|
+
Parameters
|
|
294
|
+
----------
|
|
295
|
+
x:
|
|
296
|
+
The node that may be doubly-black after deletion.
|
|
297
|
+
"""
|
|
298
|
+
while x is not self._root and x.color == BLACK:
|
|
299
|
+
parent = x.parent
|
|
300
|
+
if _is_nil(parent):
|
|
301
|
+
break
|
|
302
|
+
if x is parent.left:
|
|
303
|
+
w = parent.right
|
|
304
|
+
if not _is_nil(w) and w.color == RED:
|
|
305
|
+
# Case 1
|
|
306
|
+
w.color = BLACK
|
|
307
|
+
parent.color = RED
|
|
308
|
+
new_root = rotate(parent, "left", self._root)
|
|
309
|
+
if _is_nil(new_root.parent):
|
|
310
|
+
self._root = new_root
|
|
311
|
+
w = x.parent.right
|
|
312
|
+
|
|
313
|
+
w_left_black = _is_nil(w.left) or w.left.color == BLACK
|
|
314
|
+
w_right_black = _is_nil(w.right) or w.right.color == BLACK
|
|
315
|
+
|
|
316
|
+
if w_left_black and w_right_black:
|
|
317
|
+
# Case 2
|
|
318
|
+
if not _is_nil(w):
|
|
319
|
+
w.color = RED
|
|
320
|
+
x = x.parent
|
|
321
|
+
else:
|
|
322
|
+
if w_right_black:
|
|
323
|
+
# Case 3
|
|
324
|
+
if not _is_nil(w.left):
|
|
325
|
+
w.left.color = BLACK
|
|
326
|
+
if not _is_nil(w):
|
|
327
|
+
w.color = RED
|
|
328
|
+
new_root = rotate(w, "right", self._root)
|
|
329
|
+
if _is_nil(new_root.parent):
|
|
330
|
+
self._root = new_root
|
|
331
|
+
w = x.parent.right
|
|
332
|
+
|
|
333
|
+
# Case 4
|
|
334
|
+
if not _is_nil(w):
|
|
335
|
+
w.color = x.parent.color
|
|
336
|
+
x.parent.color = BLACK
|
|
337
|
+
if not _is_nil(w) and not _is_nil(w.right):
|
|
338
|
+
w.right.color = BLACK
|
|
339
|
+
new_root = rotate(x.parent, "left", self._root)
|
|
340
|
+
if _is_nil(new_root.parent):
|
|
341
|
+
self._root = new_root
|
|
342
|
+
x = self._root
|
|
343
|
+
else:
|
|
344
|
+
w = parent.left
|
|
345
|
+
if not _is_nil(w) and w.color == RED:
|
|
346
|
+
# Case 1 (mirror)
|
|
347
|
+
w.color = BLACK
|
|
348
|
+
parent.color = RED
|
|
349
|
+
new_root = rotate(parent, "right", self._root)
|
|
350
|
+
if _is_nil(new_root.parent):
|
|
351
|
+
self._root = new_root
|
|
352
|
+
w = x.parent.left
|
|
353
|
+
|
|
354
|
+
w_left_black = _is_nil(w.left) or w.left.color == BLACK
|
|
355
|
+
w_right_black = _is_nil(w.right) or w.right.color == BLACK
|
|
356
|
+
|
|
357
|
+
if w_right_black and w_left_black:
|
|
358
|
+
# Case 2 (mirror)
|
|
359
|
+
if not _is_nil(w):
|
|
360
|
+
w.color = RED
|
|
361
|
+
x = x.parent
|
|
362
|
+
else:
|
|
363
|
+
if w_left_black:
|
|
364
|
+
# Case 3 (mirror)
|
|
365
|
+
if not _is_nil(w.right):
|
|
366
|
+
w.right.color = BLACK
|
|
367
|
+
if not _is_nil(w):
|
|
368
|
+
w.color = RED
|
|
369
|
+
new_root = rotate(w, "left", self._root)
|
|
370
|
+
if _is_nil(new_root.parent):
|
|
371
|
+
self._root = new_root
|
|
372
|
+
w = x.parent.left
|
|
373
|
+
|
|
374
|
+
# Case 4 (mirror)
|
|
375
|
+
if not _is_nil(w):
|
|
376
|
+
w.color = x.parent.color
|
|
377
|
+
x.parent.color = BLACK
|
|
378
|
+
if not _is_nil(w) and not _is_nil(w.left):
|
|
379
|
+
w.left.color = BLACK
|
|
380
|
+
new_root = rotate(x.parent, "right", self._root)
|
|
381
|
+
if _is_nil(new_root.parent):
|
|
382
|
+
self._root = new_root
|
|
383
|
+
x = self._root
|
|
384
|
+
|
|
385
|
+
x.color = BLACK
|
|
386
|
+
|
|
387
|
+
def search(self, key: Any) -> Any | None:
|
|
388
|
+
"""Return the value for *key*, or *None* if absent.
|
|
389
|
+
|
|
390
|
+
Parameters
|
|
391
|
+
----------
|
|
392
|
+
key:
|
|
393
|
+
Key to look up.
|
|
394
|
+
"""
|
|
395
|
+
with self._lock:
|
|
396
|
+
node = self._find(self._root, key)
|
|
397
|
+
return node.value if not _is_nil(node) else None
|
|
398
|
+
|
|
399
|
+
def contains(self, key: Any) -> bool:
|
|
400
|
+
"""Return *True* if *key* is present."""
|
|
401
|
+
with self._lock:
|
|
402
|
+
return not _is_nil(self._find(self._root, key))
|
|
403
|
+
|
|
404
|
+
def update(self, key: Any, value: Any) -> None:
|
|
405
|
+
"""Update the value of an existing *key*.
|
|
406
|
+
|
|
407
|
+
Parameters
|
|
408
|
+
----------
|
|
409
|
+
key:
|
|
410
|
+
Key to update.
|
|
411
|
+
value:
|
|
412
|
+
New value.
|
|
413
|
+
|
|
414
|
+
Raises
|
|
415
|
+
------
|
|
416
|
+
KeyError
|
|
417
|
+
If *key* does not exist.
|
|
418
|
+
"""
|
|
419
|
+
with self._lock:
|
|
420
|
+
node = self._find(self._root, key)
|
|
421
|
+
if _is_nil(node):
|
|
422
|
+
raise KeyError(key)
|
|
423
|
+
node.value = value
|
|
424
|
+
|
|
425
|
+
def clear(self) -> None:
|
|
426
|
+
"""Remove all nodes from the tree."""
|
|
427
|
+
with self._lock:
|
|
428
|
+
self._root = _NIL
|
|
429
|
+
self._size = 0
|
|
430
|
+
|
|
431
|
+
# ------------------------------------------------------------------
|
|
432
|
+
# Metrics
|
|
433
|
+
# ------------------------------------------------------------------
|
|
434
|
+
|
|
435
|
+
def size(self) -> int:
|
|
436
|
+
"""Return the number of stored nodes."""
|
|
437
|
+
with self._lock:
|
|
438
|
+
return self._size
|
|
439
|
+
|
|
440
|
+
def is_empty(self) -> bool:
|
|
441
|
+
"""Return *True* if the tree is empty."""
|
|
442
|
+
with self._lock:
|
|
443
|
+
return _is_nil(self._root)
|
|
444
|
+
|
|
445
|
+
def height(self) -> int:
|
|
446
|
+
"""Return the tree height (-1 for empty, 0 for a single node)."""
|
|
447
|
+
with self._lock:
|
|
448
|
+
return self._height(self._root)
|
|
449
|
+
|
|
450
|
+
def _height(self, node: RBNode | None) -> int:
|
|
451
|
+
if _is_nil(node):
|
|
452
|
+
return -1
|
|
453
|
+
return 1 + max(self._height(node.left), self._height(node.right))
|
|
454
|
+
|
|
455
|
+
def min(self) -> Any:
|
|
456
|
+
"""Return the minimum key.
|
|
457
|
+
|
|
458
|
+
Raises
|
|
459
|
+
------
|
|
460
|
+
ValueError
|
|
461
|
+
If the tree is empty.
|
|
462
|
+
"""
|
|
463
|
+
with self._lock:
|
|
464
|
+
if _is_nil(self._root):
|
|
465
|
+
raise ValueError("Tree is empty")
|
|
466
|
+
return self._min_node(self._root).key
|
|
467
|
+
|
|
468
|
+
def max(self) -> Any:
|
|
469
|
+
"""Return the maximum key.
|
|
470
|
+
|
|
471
|
+
Raises
|
|
472
|
+
------
|
|
473
|
+
ValueError
|
|
474
|
+
If the tree is empty.
|
|
475
|
+
"""
|
|
476
|
+
with self._lock:
|
|
477
|
+
if _is_nil(self._root):
|
|
478
|
+
raise ValueError("Tree is empty")
|
|
479
|
+
return self._max_node(self._root).key
|
|
480
|
+
|
|
481
|
+
def black_height(self) -> int:
|
|
482
|
+
"""Return the black-height of the tree.
|
|
483
|
+
|
|
484
|
+
Black-height is the number of BLACK nodes on any path from the
|
|
485
|
+
root to a NIL leaf (not counting the root itself if it is RED,
|
|
486
|
+
but the root is always BLACK in a valid RB tree).
|
|
487
|
+
|
|
488
|
+
Returns
|
|
489
|
+
-------
|
|
490
|
+
int
|
|
491
|
+
Black-height ≥ 0. Returns 0 for an empty tree.
|
|
492
|
+
"""
|
|
493
|
+
with self._lock:
|
|
494
|
+
return self._black_height(self._root)
|
|
495
|
+
|
|
496
|
+
def _black_height(self, node: RBNode | None) -> int:
|
|
497
|
+
if _is_nil(node):
|
|
498
|
+
return 0
|
|
499
|
+
left_bh = self._black_height(node.left)
|
|
500
|
+
add = 1 if node.color == BLACK else 0
|
|
501
|
+
return add + left_bh
|
|
502
|
+
|
|
503
|
+
# ------------------------------------------------------------------
|
|
504
|
+
# Navigation
|
|
505
|
+
# ------------------------------------------------------------------
|
|
506
|
+
|
|
507
|
+
def predecessor(self, key: Any) -> Any | None:
|
|
508
|
+
"""Return the in-order predecessor key, or *None*.
|
|
509
|
+
|
|
510
|
+
Raises
|
|
511
|
+
------
|
|
512
|
+
KeyError
|
|
513
|
+
If *key* is not in the tree.
|
|
514
|
+
"""
|
|
515
|
+
with self._lock:
|
|
516
|
+
if not self.contains(key):
|
|
517
|
+
raise KeyError(key)
|
|
518
|
+
pred: RBNode | None = None
|
|
519
|
+
node: RBNode = self._root
|
|
520
|
+
while not _is_nil(node):
|
|
521
|
+
if key < node.key:
|
|
522
|
+
node = node.left
|
|
523
|
+
elif key > node.key:
|
|
524
|
+
pred = node
|
|
525
|
+
node = node.right
|
|
526
|
+
else:
|
|
527
|
+
if not _is_nil(node.left):
|
|
528
|
+
pred = self._max_node(node.left)
|
|
529
|
+
break
|
|
530
|
+
return pred.key if pred is not None and not _is_nil(pred) else None
|
|
531
|
+
|
|
532
|
+
def successor(self, key: Any) -> Any | None:
|
|
533
|
+
"""Return the in-order successor key, or *None*.
|
|
534
|
+
|
|
535
|
+
Raises
|
|
536
|
+
------
|
|
537
|
+
KeyError
|
|
538
|
+
If *key* is not in the tree.
|
|
539
|
+
"""
|
|
540
|
+
with self._lock:
|
|
541
|
+
if not self.contains(key):
|
|
542
|
+
raise KeyError(key)
|
|
543
|
+
succ: RBNode | None = None
|
|
544
|
+
node: RBNode = self._root
|
|
545
|
+
while not _is_nil(node):
|
|
546
|
+
if key > node.key:
|
|
547
|
+
node = node.right
|
|
548
|
+
elif key < node.key:
|
|
549
|
+
succ = node
|
|
550
|
+
node = node.left
|
|
551
|
+
else:
|
|
552
|
+
if not _is_nil(node.right):
|
|
553
|
+
succ = self._min_node(node.right)
|
|
554
|
+
break
|
|
555
|
+
return succ.key if succ is not None and not _is_nil(succ) else None
|
|
556
|
+
|
|
557
|
+
# ------------------------------------------------------------------
|
|
558
|
+
# Validation
|
|
559
|
+
# ------------------------------------------------------------------
|
|
560
|
+
|
|
561
|
+
def is_red_black_valid(self) -> bool:
|
|
562
|
+
"""Return *True* if all four Red-Black invariants hold.
|
|
563
|
+
|
|
564
|
+
Checks:
|
|
565
|
+
1. Root is BLACK.
|
|
566
|
+
2. No RED node has a RED child.
|
|
567
|
+
3. All root-to-NIL paths have the same black-height.
|
|
568
|
+
4. BST ordering is correct.
|
|
569
|
+
"""
|
|
570
|
+
with self._lock:
|
|
571
|
+
if _is_nil(self._root):
|
|
572
|
+
return True
|
|
573
|
+
if self._root.color != BLACK:
|
|
574
|
+
return False
|
|
575
|
+
if not self._check_no_red_red(self._root):
|
|
576
|
+
return False
|
|
577
|
+
bh = [None]
|
|
578
|
+
if not self._check_black_height(self._root, 0, bh):
|
|
579
|
+
return False
|
|
580
|
+
return self._check_bst_order(self._root, None, None)
|
|
581
|
+
|
|
582
|
+
def validate(self) -> bool:
|
|
583
|
+
"""Alias for :meth:`is_red_black_valid`."""
|
|
584
|
+
return self.is_red_black_valid()
|
|
585
|
+
|
|
586
|
+
def _check_no_red_red(self, node: RBNode | None) -> bool:
|
|
587
|
+
if _is_nil(node):
|
|
588
|
+
return True
|
|
589
|
+
if node.color == RED and (
|
|
590
|
+
(not _is_nil(node.left) and node.left.color == RED)
|
|
591
|
+
or (not _is_nil(node.right) and node.right.color == RED)
|
|
592
|
+
):
|
|
593
|
+
return False
|
|
594
|
+
return self._check_no_red_red(node.left) and self._check_no_red_red(node.right)
|
|
595
|
+
|
|
596
|
+
def _check_black_height(
|
|
597
|
+
self,
|
|
598
|
+
node: RBNode | None,
|
|
599
|
+
current_bh: int,
|
|
600
|
+
expected: list,
|
|
601
|
+
) -> bool:
|
|
602
|
+
if _is_nil(node):
|
|
603
|
+
if expected[0] is None:
|
|
604
|
+
expected[0] = current_bh
|
|
605
|
+
return expected[0] == current_bh
|
|
606
|
+
add = 1 if node.color == BLACK else 0
|
|
607
|
+
return self._check_black_height(
|
|
608
|
+
node.left, current_bh + add, expected
|
|
609
|
+
) and self._check_black_height(node.right, current_bh + add, expected)
|
|
610
|
+
|
|
611
|
+
def _check_bst_order(
|
|
612
|
+
self,
|
|
613
|
+
node: RBNode | None,
|
|
614
|
+
lo: Any,
|
|
615
|
+
hi: Any,
|
|
616
|
+
) -> bool:
|
|
617
|
+
if _is_nil(node):
|
|
618
|
+
return True
|
|
619
|
+
if lo is not None and node.key <= lo:
|
|
620
|
+
return False
|
|
621
|
+
if hi is not None and node.key >= hi:
|
|
622
|
+
return False
|
|
623
|
+
return self._check_bst_order(node.left, lo, node.key) and self._check_bst_order(
|
|
624
|
+
node.right, node.key, hi
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
# ------------------------------------------------------------------
|
|
628
|
+
# Copy
|
|
629
|
+
# ------------------------------------------------------------------
|
|
630
|
+
|
|
631
|
+
def copy(self) -> RedBlackTree:
|
|
632
|
+
"""Return a deep copy of this tree (new nodes, same key/value/color)."""
|
|
633
|
+
with self._lock:
|
|
634
|
+
new_tree = RedBlackTree(allow_duplicates=self._allow_duplicates)
|
|
635
|
+
new_tree._root = self._copy_node(self._root, _NIL)
|
|
636
|
+
new_tree._size = self._size
|
|
637
|
+
return new_tree
|
|
638
|
+
|
|
639
|
+
def _copy_node(
|
|
640
|
+
self,
|
|
641
|
+
node: RBNode | None,
|
|
642
|
+
parent: RBNode,
|
|
643
|
+
) -> RBNode:
|
|
644
|
+
if _is_nil(node):
|
|
645
|
+
return _NIL
|
|
646
|
+
new_node = RBNode(node.key, node.value)
|
|
647
|
+
new_node.color = node.color
|
|
648
|
+
new_node.parent = parent
|
|
649
|
+
new_node.left = self._copy_node(node.left, new_node)
|
|
650
|
+
new_node.right = self._copy_node(node.right, new_node)
|
|
651
|
+
return new_node
|
|
652
|
+
|
|
653
|
+
# ------------------------------------------------------------------
|
|
654
|
+
# Traversal helpers (private)
|
|
655
|
+
# ------------------------------------------------------------------
|
|
656
|
+
|
|
657
|
+
def _inorder(self, node: RBNode | None) -> Generator[Any, None, None]:
|
|
658
|
+
if _is_nil(node):
|
|
659
|
+
return
|
|
660
|
+
yield from self._inorder(node.left)
|
|
661
|
+
yield node.key
|
|
662
|
+
yield from self._inorder(node.right)
|
|
663
|
+
|
|
664
|
+
# ------------------------------------------------------------------
|
|
665
|
+
# Private helpers
|
|
666
|
+
# ------------------------------------------------------------------
|
|
667
|
+
|
|
668
|
+
def _find(self, node: RBNode | None, key: Any) -> RBNode:
|
|
669
|
+
while not _is_nil(node):
|
|
670
|
+
if key < node.key:
|
|
671
|
+
node = node.left
|
|
672
|
+
elif key > node.key:
|
|
673
|
+
node = node.right
|
|
674
|
+
else:
|
|
675
|
+
return node
|
|
676
|
+
return _NIL
|
|
677
|
+
|
|
678
|
+
def _min_node(self, node: RBNode) -> RBNode:
|
|
679
|
+
while not _is_nil(node.left):
|
|
680
|
+
node = node.left
|
|
681
|
+
return node
|
|
682
|
+
|
|
683
|
+
def _max_node(self, node: RBNode) -> RBNode:
|
|
684
|
+
while not _is_nil(node.right):
|
|
685
|
+
node = node.right
|
|
686
|
+
return node
|
|
687
|
+
|
|
688
|
+
# ------------------------------------------------------------------
|
|
689
|
+
# Dunder methods
|
|
690
|
+
# ------------------------------------------------------------------
|
|
691
|
+
|
|
692
|
+
def __len__(self) -> int:
|
|
693
|
+
"""Return the number of nodes in the tree."""
|
|
694
|
+
return self.size()
|
|
695
|
+
|
|
696
|
+
def __contains__(self, key: Any) -> bool:
|
|
697
|
+
"""Support ``key in tree`` syntax."""
|
|
698
|
+
return self.contains(key)
|
|
699
|
+
|
|
700
|
+
def __iter__(self) -> Iterator[Any]:
|
|
701
|
+
"""Iterate over keys in ascending (in-order) order."""
|
|
702
|
+
with self._lock:
|
|
703
|
+
keys = list(self._inorder(self._root))
|
|
704
|
+
return iter(keys)
|
|
705
|
+
|
|
706
|
+
def __repr__(self) -> str: # pragma: no cover
|
|
707
|
+
with self._lock:
|
|
708
|
+
keys = list(self._inorder(self._root))
|
|
709
|
+
bh = self.black_height()
|
|
710
|
+
return f"RedBlackTree(size={self._size}, black_height={bh}, keys={keys})"
|