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
pkstruct/trees/bst.py ADDED
@@ -0,0 +1,948 @@
1
+ """
2
+ pkstruct.trees.bst
3
+ ==================
4
+ Production-grade Binary Search Tree with a rich interview-utility API.
5
+
6
+ Architecture
7
+ ------------
8
+ - Single class ``BinarySearchTree`` backed by ``TreeNode`` from ``node.py``.
9
+ - All recursive traversal variants are dispatched through ``_traverse()``,
10
+ eliminating duplication.
11
+ - No external dependencies; Python 3.9+ only.
12
+
13
+ Public API
14
+ ----------
15
+ Core CRUD
16
+ insert, delete, search, contains, update, clear
17
+
18
+ Metrics
19
+ size, height, is_empty, min, max, floor, ceil
20
+
21
+ Navigation
22
+ predecessor, successor
23
+
24
+ Structural utilities
25
+ validate, copy, invert, is_balanced, diameter, width
26
+
27
+ Traversals (via __iter__ and explicit helpers)
28
+ inorder, preorder, postorder, level_order,
29
+ zigzag_order, vertical_order, boundary_traversal
30
+
31
+ Interview utilities
32
+ find_lca, kth_smallest, kth_largest, range_query,
33
+ path_sum, root_to_leaf_paths, serialize, deserialize
34
+
35
+ Dunder
36
+ __len__, __contains__, __iter__, __repr__
37
+
38
+ Complexity (h = tree height)
39
+ -----------------------------
40
+ Search O(h)
41
+ Insert O(h)
42
+ Delete O(h)
43
+ Space O(n)
44
+ """
45
+
46
+ from __future__ import annotations
47
+
48
+ import collections
49
+ import json
50
+ from collections.abc import Generator, Iterator
51
+ from typing import Any
52
+
53
+ from pkstruct.shared.threading import StructureLock
54
+ from pkstruct.trees.node import TreeNode
55
+
56
+
57
+ class BinarySearchTree:
58
+ """An unbalanced Binary Search Tree.
59
+
60
+ Parameters
61
+ ----------
62
+ allow_duplicates:
63
+ When *False* (default) inserting an existing key updates the value.
64
+ When *True* duplicate keys are rejected with ``ValueError``.
65
+ """
66
+
67
+ # ------------------------------------------------------------------
68
+ # Construction
69
+ # ------------------------------------------------------------------
70
+
71
+ def __init__(self, allow_duplicates: bool = False) -> None:
72
+ self._root: TreeNode | None = None
73
+ self._size: int = 0
74
+ self._allow_duplicates = allow_duplicates
75
+ self._lock: StructureLock = StructureLock()
76
+
77
+ # ------------------------------------------------------------------
78
+ # Core CRUD
79
+ # ------------------------------------------------------------------
80
+
81
+ def insert(self, key: Any, value: Any = None) -> None:
82
+ """Insert *key* with an optional *value*.
83
+
84
+ If *key* already exists and ``allow_duplicates`` is *False*, the
85
+ stored value is updated in-place (O(h)).
86
+
87
+ Parameters
88
+ ----------
89
+ key:
90
+ Comparable key used for BST ordering.
91
+ value:
92
+ Arbitrary payload stored alongside the key.
93
+
94
+ Raises
95
+ ------
96
+ ValueError
97
+ If ``allow_duplicates=True`` and *key* already exists.
98
+ """
99
+ with self._lock:
100
+ self._root, inserted = self._insert(self._root, key, value)
101
+ if inserted:
102
+ self._size += 1
103
+
104
+ def _insert(
105
+ self,
106
+ node: TreeNode | None,
107
+ key: Any,
108
+ value: Any,
109
+ ) -> tuple[TreeNode | None, bool]:
110
+ if node is None:
111
+ return TreeNode(key, value), True
112
+ current = node
113
+ parent: TreeNode | None = None
114
+ while current is not None:
115
+ parent = current
116
+ if key < current.key:
117
+ current = current.left
118
+ elif key > current.key:
119
+ current = current.right
120
+ else:
121
+ if self._allow_duplicates:
122
+ raise ValueError(f"Duplicate key: {key!r}")
123
+ current.value = value
124
+ return node, False
125
+ if key < parent.key: # type: ignore[union-attr]
126
+ parent.left = TreeNode(key, value) # type: ignore[union-attr]
127
+ else:
128
+ parent.right = TreeNode(key, value) # type: ignore[union-attr]
129
+ return node, True
130
+
131
+ def delete(self, key: Any) -> None:
132
+ """Remove the node with *key*.
133
+
134
+ Parameters
135
+ ----------
136
+ key:
137
+ Key to delete.
138
+
139
+ Raises
140
+ ------
141
+ KeyError
142
+ If *key* is not found.
143
+ """
144
+ with self._lock:
145
+ self._root, deleted = self._delete(self._root, key)
146
+ if not deleted:
147
+ raise KeyError(key)
148
+ self._size -= 1
149
+
150
+ def _delete(
151
+ self,
152
+ node: TreeNode | None,
153
+ key: Any,
154
+ ) -> tuple[TreeNode | None, bool]:
155
+ if node is None:
156
+ return None, False
157
+ if key < node.key:
158
+ node.left, deleted = self._delete(node.left, key)
159
+ elif key > node.key:
160
+ node.right, deleted = self._delete(node.right, key)
161
+ else:
162
+ deleted = True
163
+ if node.left is None:
164
+ return node.right, deleted
165
+ if node.right is None:
166
+ return node.left, deleted
167
+ # Replace with in-order successor (min of right subtree)
168
+ successor = self._min_node(node.right)
169
+ node.key = successor.key
170
+ node.value = successor.value
171
+ node.right, _ = self._delete(node.right, successor.key)
172
+ return node, deleted
173
+
174
+ def search(self, key: Any) -> Any | None:
175
+ """Return the value associated with *key*, or *None* if absent.
176
+
177
+ Parameters
178
+ ----------
179
+ key:
180
+ Key to search.
181
+
182
+ Returns
183
+ -------
184
+ Any or None
185
+ """
186
+ with self._lock:
187
+ node = self._find(self._root, key)
188
+ return node.value if node is not None else None
189
+
190
+ def contains(self, key: Any) -> bool:
191
+ """Return *True* if *key* is present in the tree."""
192
+ with self._lock:
193
+ return self._find(self._root, key) is not None
194
+
195
+ def update(self, key: Any, value: Any) -> None:
196
+ """Update the value of an existing *key*.
197
+
198
+ Parameters
199
+ ----------
200
+ key:
201
+ Key whose value should be updated.
202
+ value:
203
+ New value.
204
+
205
+ Raises
206
+ ------
207
+ KeyError
208
+ If *key* does not exist.
209
+ """
210
+ with self._lock:
211
+ node = self._find(self._root, key)
212
+ if node is None:
213
+ raise KeyError(key)
214
+ node.value = value
215
+
216
+ def clear(self) -> None:
217
+ """Remove all nodes from the tree."""
218
+ with self._lock:
219
+ self._root = None
220
+ self._size = 0
221
+
222
+ # ------------------------------------------------------------------
223
+ # Metrics
224
+ # ------------------------------------------------------------------
225
+
226
+ def size(self) -> int:
227
+ """Return the number of nodes currently in the tree."""
228
+ with self._lock:
229
+ return self._size
230
+
231
+ def height(self) -> int:
232
+ """Return the height of the tree (0 for a single-node tree, -1 if empty)."""
233
+ with self._lock:
234
+ return self._height(self._root)
235
+
236
+ def is_empty(self) -> bool:
237
+ """Return *True* if the tree contains no nodes."""
238
+ with self._lock:
239
+ return self._root is None
240
+
241
+ def min(self) -> Any:
242
+ """Return the minimum key.
243
+
244
+ Raises
245
+ ------
246
+ ValueError
247
+ If the tree is empty.
248
+ """
249
+ with self._lock:
250
+ if self._root is None:
251
+ raise ValueError("Tree is empty")
252
+ return self._min_node(self._root).key
253
+
254
+ def max(self) -> Any:
255
+ """Return the maximum key.
256
+
257
+ Raises
258
+ ------
259
+ ValueError
260
+ If the tree is empty.
261
+ """
262
+ with self._lock:
263
+ if self._root is None:
264
+ raise ValueError("Tree is empty")
265
+ return self._max_node(self._root).key
266
+
267
+ def floor(self, key: Any) -> Any | None:
268
+ """Return the largest key less than or equal to *key*, or *None*."""
269
+ with self._lock:
270
+ node = self._floor(self._root, key)
271
+ return node.key if node is not None else None
272
+
273
+ def _floor(self, node: TreeNode | None, key: Any) -> TreeNode | None:
274
+ if node is None:
275
+ return None
276
+ if key == node.key:
277
+ return node
278
+ if key < node.key:
279
+ return self._floor(node.left, key)
280
+ right = self._floor(node.right, key)
281
+ return right if right is not None else node
282
+
283
+ def _ceil(self, node: TreeNode | None, key: Any) -> TreeNode | None:
284
+ if node is None:
285
+ return None
286
+ if key == node.key:
287
+ return node
288
+ if key > node.key:
289
+ return self._ceil(node.right, key)
290
+ left = self._ceil(node.left, key)
291
+ return left if left is not None else node
292
+
293
+ def ceil(self, key: Any) -> Any | None:
294
+ """Return the smallest key greater than or equal to *key*, or *None*."""
295
+ with self._lock:
296
+ node = self._ceil(self._root, key)
297
+ return node.key if node is not None else None
298
+
299
+ # ------------------------------------------------------------------
300
+ # Navigation
301
+ # ------------------------------------------------------------------
302
+
303
+ def predecessor(self, key: Any) -> Any | None:
304
+ """Return the in-order predecessor key of *key*, or *None*.
305
+
306
+ Raises
307
+ ------
308
+ KeyError
309
+ If *key* is not in the tree.
310
+ """
311
+ with self._lock:
312
+ if not self.contains(key):
313
+ raise KeyError(key)
314
+ pred: TreeNode | None = None
315
+ node = self._root
316
+ while node is not None:
317
+ if key < node.key:
318
+ node = node.left
319
+ elif key > node.key:
320
+ pred = node
321
+ node = node.right
322
+ else:
323
+ if node.left is not None:
324
+ pred = self._max_node(node.left)
325
+ break
326
+ return pred.key if pred is not None else None
327
+
328
+ def successor(self, key: Any) -> Any | None:
329
+ """Return the in-order successor key of *key*, or *None*.
330
+
331
+ Raises
332
+ ------
333
+ KeyError
334
+ If *key* is not in the tree.
335
+ """
336
+ with self._lock:
337
+ if not self.contains(key):
338
+ raise KeyError(key)
339
+ succ: TreeNode | None = None
340
+ node = self._root
341
+ while node is not None:
342
+ if key > node.key:
343
+ node = node.right
344
+ elif key < node.key:
345
+ succ = node
346
+ node = node.left
347
+ else:
348
+ if node.right is not None:
349
+ succ = self._min_node(node.right)
350
+ break
351
+ return succ.key if succ is not None else None
352
+
353
+ # ------------------------------------------------------------------
354
+ # Structural utilities
355
+ # ------------------------------------------------------------------
356
+
357
+ def validate(self) -> bool:
358
+ """Verify BST ordering invariant for every node.
359
+
360
+ Returns
361
+ -------
362
+ bool
363
+ *True* if all nodes satisfy the BST property.
364
+ """
365
+ with self._lock:
366
+ return self._validate(self._root, None, None)
367
+
368
+ def _validate(
369
+ self,
370
+ node: TreeNode | None,
371
+ lo: Any,
372
+ hi: Any,
373
+ ) -> bool:
374
+ if node is None:
375
+ return True
376
+ if lo is not None and node.key <= lo:
377
+ return False
378
+ if hi is not None and node.key >= hi:
379
+ return False
380
+ return self._validate(node.left, lo, node.key) and self._validate(node.right, node.key, hi)
381
+
382
+ def copy(self) -> BinarySearchTree:
383
+ """Return a deep copy of this tree (new nodes, same key/value pairs)."""
384
+ with self._lock:
385
+ new_tree: BinarySearchTree = BinarySearchTree(allow_duplicates=self._allow_duplicates)
386
+ new_tree._root = self._copy_node(self._root)
387
+ new_tree._size = self._size
388
+ return new_tree
389
+
390
+ def _copy_node(self, node: TreeNode | None) -> TreeNode | None:
391
+ if node is None:
392
+ return None
393
+ new_node = TreeNode(node.key, node.value)
394
+ new_node.left = self._copy_node(node.left)
395
+ new_node.right = self._copy_node(node.right)
396
+ return new_node
397
+
398
+ def invert(self) -> None:
399
+ """Mirror the tree in-place (swap left/right children at every node)."""
400
+ with self._lock:
401
+ self._invert(self._root)
402
+
403
+ def _invert(self, node: TreeNode | None) -> None:
404
+ if node is None:
405
+ return
406
+ node.left, node.right = node.right, node.left
407
+ self._invert(node.left)
408
+ self._invert(node.right)
409
+
410
+ def is_balanced(self) -> bool:
411
+ """Return *True* if the tree is height-balanced (|left_h - right_h| ≤ 1 for every node)."""
412
+ with self._lock:
413
+ return self._check_balanced(self._root) != -2
414
+
415
+ def _check_balanced(self, node: TreeNode | None) -> int:
416
+ """Return height if balanced, -2 as sentinel for unbalanced."""
417
+ if node is None:
418
+ return -1
419
+ lh = self._check_balanced(node.left)
420
+ if lh == -2:
421
+ return -2
422
+ rh = self._check_balanced(node.right)
423
+ if rh == -2:
424
+ return -2
425
+ if abs(lh - rh) > 1:
426
+ return -2
427
+ return max(lh, rh) + 1
428
+
429
+ def diameter(self) -> int:
430
+ """Return the diameter (longest path between any two nodes, in edges)."""
431
+ with self._lock:
432
+ self._diameter_max: int = 0
433
+ self._diameter_helper(self._root)
434
+ return self._diameter_max
435
+
436
+ def _diameter_helper(self, node: TreeNode | None) -> int:
437
+ if node is None:
438
+ return -1
439
+ lh = self._diameter_helper(node.left) + 1
440
+ rh = self._diameter_helper(node.right) + 1
441
+ self._diameter_max = max(self._diameter_max, lh + rh)
442
+ return max(lh, rh)
443
+
444
+ def width(self) -> int:
445
+ """Return the maximum width (number of nodes) across all levels."""
446
+ with self._lock:
447
+ if self._root is None:
448
+ return 0
449
+ max_w = 0
450
+ queue: collections.deque[TreeNode] = collections.deque([self._root])
451
+ while queue:
452
+ level_size = len(queue)
453
+ max_w = max(max_w, level_size)
454
+ for _ in range(level_size):
455
+ node = queue.popleft()
456
+ if node.left:
457
+ queue.append(node.left)
458
+ if node.right:
459
+ queue.append(node.right)
460
+ return max_w
461
+
462
+ # ------------------------------------------------------------------
463
+ # Interview utilities
464
+ # ------------------------------------------------------------------
465
+
466
+ def find_lca(self, key1: Any, key2: Any) -> Any | None:
467
+ """Return the key of the Lowest Common Ancestor of *key1* and *key2*.
468
+
469
+ Parameters
470
+ ----------
471
+ key1, key2:
472
+ Both must exist in the tree.
473
+
474
+ Raises
475
+ ------
476
+ KeyError
477
+ If either key is absent.
478
+ """
479
+ with self._lock:
480
+ for k in (key1, key2):
481
+ if not self.contains(k):
482
+ raise KeyError(k)
483
+ node = self._lca(self._root, key1, key2)
484
+ return node.key if node is not None else None
485
+
486
+ def _lca(
487
+ self,
488
+ node: TreeNode | None,
489
+ k1: Any,
490
+ k2: Any,
491
+ ) -> TreeNode | None:
492
+ if node is None:
493
+ return None
494
+ if k1 < node.key and k2 < node.key:
495
+ return self._lca(node.left, k1, k2)
496
+ if k1 > node.key and k2 > node.key:
497
+ return self._lca(node.right, k1, k2)
498
+ return node
499
+
500
+ def kth_smallest(self, k: int) -> Any:
501
+ """Return the k-th smallest key (1-indexed).
502
+
503
+ Raises
504
+ ------
505
+ ValueError
506
+ If *k* is out of range.
507
+ """
508
+ with self._lock:
509
+ result: list[Any] = []
510
+ self._inorder_collect(self._root, result, k)
511
+ if k < 1 or k > len(result):
512
+ raise ValueError(f"k={k} is out of range [1, {self._size}]")
513
+ return result[k - 1]
514
+
515
+ def kth_largest(self, k: int) -> Any:
516
+ """Return the k-th largest key (1-indexed).
517
+
518
+ Raises
519
+ ------
520
+ ValueError
521
+ If *k* is out of range.
522
+ """
523
+ with self._lock:
524
+ if k < 1 or k > self._size:
525
+ raise ValueError(f"k={k} is out of range [1, {self._size}]")
526
+ # Reverse in-order (right → root → left)
527
+ result: list[Any] = []
528
+ self._reverse_inorder_collect(self._root, result, k)
529
+ return result[k - 1]
530
+
531
+ def _reverse_inorder_collect(
532
+ self,
533
+ node: TreeNode | None,
534
+ result: list,
535
+ limit: int,
536
+ ) -> None:
537
+ if node is None or len(result) >= limit:
538
+ return
539
+ self._reverse_inorder_collect(node.right, result, limit)
540
+ if len(result) < limit:
541
+ result.append(node.key)
542
+ self._reverse_inorder_collect(node.left, result, limit)
543
+
544
+ def range_query(self, lo: Any, hi: Any) -> list[Any]:
545
+ """Return all keys in [*lo*, *hi*] in sorted order.
546
+
547
+ Parameters
548
+ ----------
549
+ lo, hi:
550
+ Inclusive lower and upper bounds.
551
+ """
552
+ with self._lock:
553
+ result: list[Any] = []
554
+ self._range_collect(self._root, lo, hi, result)
555
+ return result
556
+
557
+ def _range_collect(
558
+ self,
559
+ node: TreeNode | None,
560
+ lo: Any,
561
+ hi: Any,
562
+ result: list,
563
+ ) -> None:
564
+ if node is None:
565
+ return
566
+ if lo < node.key:
567
+ self._range_collect(node.left, lo, hi, result)
568
+ if lo <= node.key <= hi:
569
+ result.append(node.key)
570
+ if hi > node.key:
571
+ self._range_collect(node.right, lo, hi, result)
572
+
573
+ def path_sum(self, target: int | float) -> bool:
574
+ """Return *True* if any root-to-leaf path sums to *target*.
575
+
576
+ Assumes keys are numeric.
577
+ """
578
+ with self._lock:
579
+ return self._path_sum(self._root, target, 0)
580
+
581
+ def _path_sum(
582
+ self,
583
+ node: TreeNode | None,
584
+ target: int | float,
585
+ current: int | float,
586
+ ) -> bool:
587
+ if node is None:
588
+ return False
589
+ current += node.key
590
+ if node.left is None and node.right is None:
591
+ return current == target
592
+ return self._path_sum(node.left, target, current) or self._path_sum(
593
+ node.right, target, current
594
+ )
595
+
596
+ def root_to_leaf_paths(self) -> list[list[Any]]:
597
+ """Return all root-to-leaf paths as lists of keys."""
598
+ with self._lock:
599
+ paths: list[list[Any]] = []
600
+ self._collect_paths(self._root, [], paths)
601
+ return paths
602
+
603
+ def _collect_paths(
604
+ self,
605
+ node: TreeNode | None,
606
+ current: list[Any],
607
+ paths: list[list[Any]],
608
+ ) -> None:
609
+ if node is None:
610
+ return
611
+ current.append(node.key)
612
+ if node.left is None and node.right is None:
613
+ paths.append(list(current))
614
+ else:
615
+ self._collect_paths(node.left, current, paths)
616
+ self._collect_paths(node.right, current, paths)
617
+ current.pop()
618
+
619
+ def serialize(self) -> str:
620
+ """Serialize the tree to a JSON string (level-order with null sentinels).
621
+
622
+ Returns
623
+ -------
624
+ str
625
+ JSON representation that can be passed to :meth:`deserialize`.
626
+ """
627
+ with self._lock:
628
+ if self._root is None:
629
+ return "[]"
630
+ result: list[Any | None] = []
631
+ queue: collections.deque[TreeNode | None] = collections.deque([self._root])
632
+ while queue:
633
+ node = queue.popleft()
634
+ if node is None:
635
+ result.append(None)
636
+ else:
637
+ result.append(node.key)
638
+ queue.append(node.left)
639
+ queue.append(node.right)
640
+ # Trim trailing nulls
641
+ while result and result[-1] is None:
642
+ result.pop()
643
+ return json.dumps(result)
644
+
645
+ def deserialize(self, data: str) -> None:
646
+ """Rebuild the tree from a JSON string produced by :meth:`serialize`.
647
+
648
+ Clears any existing content before loading.
649
+
650
+ Parameters
651
+ ----------
652
+ data:
653
+ JSON string as returned by :meth:`serialize`.
654
+ """
655
+ with self._lock:
656
+ self.clear()
657
+ keys: list[Any | None] = json.loads(data)
658
+ if not keys:
659
+ return
660
+ self._root = TreeNode(keys[0])
661
+ self._size = 1
662
+ queue: collections.deque[TreeNode] = collections.deque([self._root])
663
+ i = 1
664
+ while queue and i < len(keys):
665
+ node = queue.popleft()
666
+ if i < len(keys) and keys[i] is not None:
667
+ node.left = TreeNode(keys[i])
668
+ self._size += 1
669
+ queue.append(node.left)
670
+ i += 1
671
+ if i < len(keys) and keys[i] is not None:
672
+ node.right = TreeNode(keys[i])
673
+ self._size += 1
674
+ queue.append(node.right)
675
+ i += 1
676
+
677
+ def boundary_traversal(self) -> list[Any]:
678
+ """Return keys in boundary order: left boundary + leaves + right boundary (reversed).
679
+
680
+ The result traces the outer edge of the tree anti-clockwise starting
681
+ from the root.
682
+ """
683
+ with self._lock:
684
+ if self._root is None:
685
+ return []
686
+ result: list[Any] = [self._root.key]
687
+ self._left_boundary(self._root.left, result)
688
+ self._leaves(self._root.left, result)
689
+ self._leaves(self._root.right, result)
690
+ self._right_boundary(self._root.right, result)
691
+ return result
692
+
693
+ def vertical_order(self) -> list[list[Any]]:
694
+ """Return keys grouped by vertical column, left to right.
695
+
696
+ Each inner list contains the keys at the same horizontal distance
697
+ from the root, sorted top-to-bottom within the column.
698
+ """
699
+ with self._lock:
700
+ if self._root is None:
701
+ return []
702
+ col_map: dict[int, list[Any]] = collections.defaultdict(list)
703
+ queue: collections.deque[tuple[TreeNode, int]] = collections.deque([(self._root, 0)])
704
+ while queue:
705
+ node, col = queue.popleft()
706
+ col_map[col].append(node.key)
707
+ if node.left:
708
+ queue.append((node.left, col - 1))
709
+ if node.right:
710
+ queue.append((node.right, col + 1))
711
+ return [col_map[c] for c in sorted(col_map)]
712
+
713
+ def zigzag_order(self) -> list[list[Any]]:
714
+ """Return keys level by level, alternating left-to-right and right-to-left."""
715
+ with self._lock:
716
+ if self._root is None:
717
+ return []
718
+ result: list[list[Any]] = []
719
+ queue: collections.deque[TreeNode] = collections.deque([self._root])
720
+ left_to_right = True
721
+ while queue:
722
+ level_size = len(queue)
723
+ level: collections.deque[Any] = collections.deque()
724
+ for _ in range(level_size):
725
+ node = queue.popleft()
726
+ if left_to_right:
727
+ level.append(node.key)
728
+ else:
729
+ level.appendleft(node.key)
730
+ if node.left:
731
+ queue.append(node.left)
732
+ if node.right:
733
+ queue.append(node.right)
734
+ result.append(list(level))
735
+ left_to_right = not left_to_right
736
+ return result
737
+
738
+ def _left_boundary(self, node: TreeNode | None, result: list) -> None:
739
+ if node is None or (node.left is None and node.right is None):
740
+ return
741
+ result.append(node.key)
742
+ if node.left:
743
+ self._left_boundary(node.left, result)
744
+ else:
745
+ self._left_boundary(node.right, result)
746
+
747
+ def _right_boundary(self, node: TreeNode | None, result: list) -> None:
748
+ if node is None or (node.left is None and node.right is None):
749
+ return
750
+ if node.right:
751
+ self._right_boundary(node.right, result)
752
+ else:
753
+ self._right_boundary(node.left, result)
754
+ result.append(node.key)
755
+
756
+ def _leaves(self, node: TreeNode | None, result: list) -> None:
757
+ if node is None:
758
+ return
759
+ if node.left is None and node.right is None:
760
+ result.append(node.key)
761
+ return
762
+ self._leaves(node.left, result)
763
+ self._leaves(node.right, result)
764
+
765
+ def vertical_order(self) -> list[list[Any]]:
766
+ """Return keys grouped by vertical column, left to right.
767
+
768
+ Each inner list contains the keys at the same horizontal distance
769
+ from the root, sorted top-to-bottom within the column.
770
+ """
771
+ if self._root is None:
772
+ return []
773
+ col_map: dict[int, list[Any]] = collections.defaultdict(list)
774
+ queue: collections.deque[tuple[TreeNode, int]] = collections.deque([(self._root, 0)])
775
+ while queue:
776
+ node, col = queue.popleft()
777
+ col_map[col].append(node.key)
778
+ if node.left:
779
+ queue.append((node.left, col - 1))
780
+ if node.right:
781
+ queue.append((node.right, col + 1))
782
+ return [col_map[c] for c in sorted(col_map)]
783
+
784
+ def zigzag_order(self) -> list[list[Any]]:
785
+ """Return keys level by level, alternating left-to-right and right-to-left."""
786
+ if self._root is None:
787
+ return []
788
+ result: list[list[Any]] = []
789
+ queue: collections.deque[TreeNode] = collections.deque([self._root])
790
+ left_to_right = True
791
+ while queue:
792
+ level_size = len(queue)
793
+ level: collections.deque[Any] = collections.deque()
794
+ for _ in range(level_size):
795
+ node = queue.popleft()
796
+ if left_to_right:
797
+ level.append(node.key)
798
+ else:
799
+ level.appendleft(node.key)
800
+ if node.left:
801
+ queue.append(node.left)
802
+ if node.right:
803
+ queue.append(node.right)
804
+ result.append(list(level))
805
+ left_to_right = not left_to_right
806
+ return result
807
+
808
+ # ------------------------------------------------------------------
809
+ # Traversal helpers
810
+ # ------------------------------------------------------------------
811
+
812
+ def _traverse(
813
+ self,
814
+ order: str = "inorder",
815
+ ) -> Generator[Any, None, None]:
816
+ """Unified traversal generator.
817
+
818
+ Parameters
819
+ ----------
820
+ order:
821
+ One of ``"inorder"``, ``"preorder"``, ``"postorder"``,
822
+ ``"levelorder"``.
823
+ """
824
+ if order == "inorder":
825
+ yield from self._inorder(self._root)
826
+ elif order == "preorder":
827
+ yield from self._preorder(self._root)
828
+ elif order == "postorder":
829
+ yield from self._postorder(self._root)
830
+ elif order == "levelorder":
831
+ yield from self._levelorder()
832
+ else:
833
+ raise ValueError(f"Unknown traversal order: {order!r}")
834
+
835
+ def _inorder(self, node: TreeNode | None) -> Generator[Any, None, None]:
836
+ stack: list[TreeNode] = []
837
+ current = node
838
+ while stack or current:
839
+ while current:
840
+ stack.append(current)
841
+ current = current.left
842
+ current = stack.pop()
843
+ yield current.key
844
+ current = current.right
845
+
846
+ def _preorder(self, node: TreeNode | None) -> Generator[Any, None, None]:
847
+ if node is None:
848
+ return
849
+ stack: list[TreeNode] = [node]
850
+ while stack:
851
+ current = stack.pop()
852
+ yield current.key
853
+ if current.right:
854
+ stack.append(current.right)
855
+ if current.left:
856
+ stack.append(current.left)
857
+
858
+ def _postorder(self, node: TreeNode | None) -> Generator[Any, None, None]:
859
+ if node is None:
860
+ return
861
+ stack1: list[TreeNode] = [node]
862
+ stack2: list[TreeNode] = []
863
+ while stack1:
864
+ current = stack1.pop()
865
+ stack2.append(current)
866
+ if current.left:
867
+ stack1.append(current.left)
868
+ if current.right:
869
+ stack1.append(current.right)
870
+ while stack2:
871
+ yield stack2.pop().key
872
+
873
+ def _levelorder(self) -> Generator[Any, None, None]:
874
+ if self._root is None:
875
+ return
876
+ queue: collections.deque[TreeNode] = collections.deque([self._root])
877
+ while queue:
878
+ node = queue.popleft()
879
+ yield node.key
880
+ if node.left:
881
+ queue.append(node.left)
882
+ if node.right:
883
+ queue.append(node.right)
884
+
885
+ def _inorder_collect(
886
+ self,
887
+ node: TreeNode | None,
888
+ result: list,
889
+ limit: int,
890
+ ) -> None:
891
+ if node is None or len(result) >= limit:
892
+ return
893
+ self._inorder_collect(node.left, result, limit)
894
+ if len(result) < limit:
895
+ result.append(node.key)
896
+ self._inorder_collect(node.right, result, limit)
897
+
898
+ # ------------------------------------------------------------------
899
+ # Private helpers
900
+ # ------------------------------------------------------------------
901
+
902
+ def _find(self, node: TreeNode | None, key: Any) -> TreeNode | None:
903
+ while node is not None:
904
+ if key < node.key:
905
+ node = node.left
906
+ elif key > node.key:
907
+ node = node.right
908
+ else:
909
+ return node
910
+ return None
911
+
912
+ def _height(self, node: TreeNode | None) -> int:
913
+ if node is None:
914
+ return -1
915
+ return 1 + max(self._height(node.left), self._height(node.right))
916
+
917
+ def _min_node(self, node: TreeNode) -> TreeNode:
918
+ while node.left is not None:
919
+ node = node.left
920
+ return node
921
+
922
+ def _max_node(self, node: TreeNode) -> TreeNode:
923
+ while node.right is not None:
924
+ node = node.right
925
+ return node
926
+
927
+ # ------------------------------------------------------------------
928
+ # Dunder methods
929
+ # ------------------------------------------------------------------
930
+
931
+ def __len__(self) -> int:
932
+ """Return the number of nodes in the tree."""
933
+ return self.size()
934
+
935
+ def __contains__(self, key: Any) -> bool:
936
+ """Support ``key in tree`` syntax."""
937
+ return self.contains(key)
938
+
939
+ def __iter__(self) -> Iterator[Any]:
940
+ """Iterate over keys in ascending (in-order) order."""
941
+ with self._lock:
942
+ keys = list(self._traverse("inorder"))
943
+ return iter(keys)
944
+
945
+ def __repr__(self) -> str: # pragma: no cover
946
+ with self._lock:
947
+ keys = list(self._traverse("inorder"))
948
+ return f"BinarySearchTree(size={self._size}, keys={keys})"