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,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})"