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,541 @@
1
+ """
2
+ Interval Tree for efficient overlap queries and point containment.
3
+
4
+ Architecture:
5
+ - Augmented BST (AVL-balanced) where each node stores an interval (start, end)
6
+ and a subtree max_endpoint value for O(log n) overlap pruning.
7
+ - AVL rotations keep height O(log n), giving O(log n) insert/delete/search.
8
+ - merge_overlaps operates in O(n log n) by collecting + re-inserting.
9
+ - No external dependencies; does not reuse balancing.py to stay self-contained
10
+ (the interval augmentation requires custom metadata propagation incompatible
11
+ with the generic rotate() signature in balancing.py).
12
+
13
+ Public API:
14
+ IntervalTree()
15
+ .insert(start, end)
16
+ .delete(start, end)
17
+ .search(start, end) -> list of overlapping intervals
18
+ .overlap(start, end) -> bool
19
+ .all_overlaps(start, end) -> list of all overlapping intervals
20
+ .contains_point(point) -> list of intervals containing point
21
+ .merge_overlaps() -> merges and rebuilds tree
22
+ .validate()
23
+ .clear()
24
+ .size
25
+ .height()
26
+ __len__, __contains__, __iter__, __repr__
27
+
28
+ Complexity:
29
+ Insert: O(log n)
30
+ Delete: O(log n)
31
+ Search: O(log n + k) where k = number of results
32
+ overlap check: O(log n)
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ from collections.abc import Iterator
38
+
39
+ from pkstruct.shared.threading import StructureLock
40
+
41
+ Interval = tuple[int, int]
42
+
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Internal node
46
+ # ---------------------------------------------------------------------------
47
+
48
+
49
+ class _INode:
50
+ """Internal node for the augmented AVL interval tree."""
51
+
52
+ __slots__ = ("start", "end", "max_end", "height", "left", "right")
53
+
54
+ def __init__(self, start: int, end: int) -> None:
55
+ self.start: int = start
56
+ self.end: int = end
57
+ self.max_end: int = end # max endpoint in subtree
58
+ self.height: int = 1
59
+ self.left: _INode | None = None
60
+ self.right: _INode | None = None
61
+
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # AVL helpers (interval-specific)
65
+ # ---------------------------------------------------------------------------
66
+
67
+
68
+ def _height(node: _INode | None) -> int:
69
+ return node.height if node is not None else 0
70
+
71
+
72
+ def _max_end(node: _INode | None) -> int:
73
+ return node.max_end if node is not None else -(2**62)
74
+
75
+
76
+ def _update(node: _INode) -> None:
77
+ """Recompute height and max_end from children."""
78
+ node.height = 1 + max(_height(node.left), _height(node.right))
79
+ node.max_end = max(node.end, _max_end(node.left), _max_end(node.right))
80
+
81
+
82
+ def _balance_factor(node: _INode) -> int:
83
+ return _height(node.left) - _height(node.right)
84
+
85
+
86
+ def _rotate_right(y: _INode) -> _INode:
87
+ x = y.left
88
+ assert x is not None
89
+ t2 = x.right
90
+ x.right = y
91
+ y.left = t2
92
+ _update(y)
93
+ _update(x)
94
+ return x
95
+
96
+
97
+ def _rotate_left(x: _INode) -> _INode:
98
+ y = x.right
99
+ assert y is not None
100
+ t2 = y.left
101
+ y.left = x
102
+ x.right = t2
103
+ _update(x)
104
+ _update(y)
105
+ return y
106
+
107
+
108
+ def _rebalance(node: _INode) -> _INode:
109
+ _update(node)
110
+ bf = _balance_factor(node)
111
+
112
+ # Left heavy
113
+ if bf > 1:
114
+ assert node.left is not None
115
+ if _balance_factor(node.left) < 0:
116
+ node.left = _rotate_left(node.left)
117
+ return _rotate_right(node)
118
+
119
+ # Right heavy
120
+ if bf < -1:
121
+ assert node.right is not None
122
+ if _balance_factor(node.right) > 0:
123
+ node.right = _rotate_right(node.right)
124
+ return _rotate_left(node)
125
+
126
+ return node
127
+
128
+
129
+ # ---------------------------------------------------------------------------
130
+ # BST operations
131
+ # ---------------------------------------------------------------------------
132
+
133
+
134
+ def _insert(node: _INode | None, start: int, end: int) -> _INode:
135
+ """Insert (start, end) into subtree rooted at node; return new root."""
136
+ if node is None:
137
+ return _INode(start, end)
138
+ if (start, end) < (node.start, node.end):
139
+ node.left = _insert(node.left, start, end)
140
+ else:
141
+ node.right = _insert(node.right, start, end)
142
+ return _rebalance(node)
143
+
144
+
145
+ def _min_node(node: _INode) -> _INode:
146
+ while node.left is not None:
147
+ node = node.left
148
+ return node
149
+
150
+
151
+ def _delete(node: _INode | None, start: int, end: int) -> _INode | None:
152
+ """Delete first occurrence of (start, end); return new root."""
153
+ if node is None:
154
+ return None
155
+ if (start, end) < (node.start, node.end):
156
+ node.left = _delete(node.left, start, end)
157
+ elif (start, end) > (node.start, node.end):
158
+ node.right = _delete(node.right, start, end)
159
+ else:
160
+ # Found
161
+ if node.left is None:
162
+ return node.right
163
+ if node.right is None:
164
+ return node.left
165
+ successor = _min_node(node.right)
166
+ node.start, node.end = successor.start, successor.end
167
+ node.right = _delete(node.right, successor.start, successor.end)
168
+
169
+ return _rebalance(node)
170
+
171
+
172
+ # ---------------------------------------------------------------------------
173
+ # Overlap search helpers
174
+ # ---------------------------------------------------------------------------
175
+
176
+
177
+ def _overlaps(a_start: int, a_end: int, b_start: int, b_end: int) -> bool:
178
+ """True if intervals [a_start, a_end] and [b_start, b_end] overlap."""
179
+ return a_start <= b_end and b_start <= a_end
180
+
181
+
182
+ def _search_overlaps(
183
+ node: _INode | None,
184
+ start: int,
185
+ end: int,
186
+ results: list[Interval],
187
+ ) -> None:
188
+ """Collect all intervals overlapping [start, end] into results."""
189
+ if node is None:
190
+ return
191
+ # Prune: if max_end of left subtree < start, no overlap in left subtree
192
+ if _overlaps(node.start, node.end, start, end):
193
+ results.append((node.start, node.end))
194
+ if node.left is not None and node.left.max_end >= start:
195
+ _search_overlaps(node.left, start, end, results)
196
+ _search_overlaps(node.right, start, end, results)
197
+
198
+
199
+ def _contains_point(
200
+ node: _INode | None,
201
+ point: int,
202
+ results: list[Interval],
203
+ ) -> None:
204
+ """Collect all intervals containing point."""
205
+ if node is None:
206
+ return
207
+ if node.max_end < point:
208
+ return
209
+ if node.start <= point <= node.end:
210
+ results.append((node.start, node.end))
211
+ _contains_point(node.left, point, results)
212
+ _contains_point(node.right, point, results)
213
+
214
+
215
+ def _inorder(node: _INode | None, out: list[Interval]) -> None:
216
+ if node is None:
217
+ return
218
+ _inorder(node.left, out)
219
+ out.append((node.start, node.end))
220
+ _inorder(node.right, out)
221
+
222
+
223
+ # ---------------------------------------------------------------------------
224
+ # IntervalTree
225
+ # ---------------------------------------------------------------------------
226
+
227
+
228
+ class IntervalTree:
229
+ """
230
+ An augmented AVL interval tree supporting efficient overlap queries.
231
+
232
+ Each node stores an interval (start, end) and the maximum endpoint
233
+ in its subtree (max_end), enabling O(log n) overlap pruning.
234
+
235
+ Intervals are inclusive: [start, end].
236
+
237
+ Args:
238
+ None
239
+
240
+ Example:
241
+ >>> it = IntervalTree()
242
+ >>> it.insert(15, 20)
243
+ >>> it.insert(10, 30)
244
+ >>> it.insert(17, 19)
245
+ >>> it.search(14, 16)
246
+ [(15, 20), (10, 30)]
247
+ >>> (17, 19) in it
248
+ True
249
+ >>> it.contains_point(18)
250
+ [(15, 20), (10, 30), (17, 19)]
251
+ """
252
+
253
+ def __init__(self) -> None:
254
+ self._root: _INode | None = None
255
+ self._size: int = 0
256
+ self._lock: StructureLock = StructureLock()
257
+
258
+ # ------------------------------------------------------------------
259
+ # Insert / delete
260
+ # ------------------------------------------------------------------
261
+
262
+ def insert(self, start: int, end: int) -> None:
263
+ """
264
+ Insert interval [start, end] into the tree.
265
+
266
+ Complexity: O(log n)
267
+
268
+ Args:
269
+ start: Interval start (inclusive).
270
+ end: Interval end (inclusive).
271
+
272
+ Raises:
273
+ ValueError: If start > end.
274
+ """
275
+ with self._lock:
276
+ if start > end:
277
+ raise ValueError(f"Invalid interval: start ({start}) must be <= end ({end}).")
278
+ self._root = _insert(self._root, start, end)
279
+ self._size += 1
280
+
281
+ def delete(self, start: int, end: int) -> None:
282
+ """
283
+ Delete one occurrence of interval [start, end] from the tree.
284
+
285
+ Complexity: O(log n)
286
+
287
+ Args:
288
+ start: Interval start.
289
+ end: Interval end.
290
+
291
+ Raises:
292
+ KeyError: If the interval is not present.
293
+ """
294
+ with self._lock:
295
+ if not self.__contains__((start, end)):
296
+ raise KeyError(f"Interval ({start}, {end}) not found in tree.")
297
+ self._root = _delete(self._root, start, end)
298
+ self._size -= 1
299
+
300
+ # ------------------------------------------------------------------
301
+ # Search / overlap
302
+ # ------------------------------------------------------------------
303
+
304
+ def search(self, start: int, end: int) -> list[Interval]:
305
+ """
306
+ Return a list of intervals that overlap with [start, end].
307
+
308
+ Two intervals overlap if they share at least one point.
309
+
310
+ Complexity: O(log n + k) where k is the number of results.
311
+
312
+ Args:
313
+ start: Query interval start.
314
+ end: Query interval end.
315
+
316
+ Returns:
317
+ List of overlapping (start, end) tuples.
318
+ """
319
+ with self._lock:
320
+ results: list[Interval] = []
321
+ _search_overlaps(self._root, start, end, results)
322
+ return results
323
+
324
+ def overlap(self, start: int, end: int) -> bool:
325
+ """
326
+ Return True if any interval in the tree overlaps [start, end].
327
+
328
+ Complexity: O(log n)
329
+
330
+ Args:
331
+ start: Query interval start.
332
+ end: Query interval end.
333
+
334
+ Returns:
335
+ True if at least one overlap exists.
336
+ """
337
+ with self._lock:
338
+ return self._overlap_exists(self._root, start, end)
339
+
340
+ def _overlap_exists(self, node: _INode | None, start: int, end: int) -> bool:
341
+ if node is None:
342
+ return False
343
+ if _overlaps(node.start, node.end, start, end):
344
+ return True
345
+ if (
346
+ node.left is not None
347
+ and node.left.max_end >= start
348
+ and self._overlap_exists(node.left, start, end)
349
+ ):
350
+ return True
351
+ return self._overlap_exists(node.right, start, end)
352
+
353
+ def all_overlaps(self, start: int, end: int) -> list[Interval]:
354
+ """
355
+ Return ALL intervals overlapping [start, end].
356
+
357
+ Alias for search() with semantically clearer name.
358
+
359
+ Complexity: O(log n + k)
360
+ """
361
+ return self.search(start, end)
362
+
363
+ # all_overlaps delegates to search which wraps with lock
364
+
365
+ def contains_point(self, point: int) -> list[Interval]:
366
+ """
367
+ Return all intervals that contain the given point.
368
+
369
+ Complexity: O(log n + k)
370
+
371
+ Args:
372
+ point: Query point.
373
+
374
+ Returns:
375
+ List of intervals [start, end] where start <= point <= end.
376
+ """
377
+ with self._lock:
378
+ results: list[Interval] = []
379
+ _contains_point(self._root, point, results)
380
+ return results
381
+
382
+ # ------------------------------------------------------------------
383
+ # Merge overlaps
384
+ # ------------------------------------------------------------------
385
+
386
+ def merge_overlaps(self) -> None:
387
+ """
388
+ Merge all overlapping/touching intervals in-place.
389
+
390
+ The tree is rebuilt from the merged set of intervals.
391
+
392
+ Complexity: O(n log n) [sort + linear merge + rebuild]
393
+ """
394
+ with self._lock:
395
+ intervals: list[Interval] = []
396
+ _inorder(self._root, intervals)
397
+ if not intervals:
398
+ return
399
+ intervals.sort()
400
+ merged: list[Interval] = [intervals[0]]
401
+ for start, end in intervals[1:]:
402
+ prev_start, prev_end = merged[-1]
403
+ if start <= prev_end + 1: # overlapping or touching
404
+ merged[-1] = (prev_start, max(prev_end, end))
405
+ else:
406
+ merged.append((start, end))
407
+ self.clear()
408
+ for s, e in merged:
409
+ self.insert(s, e)
410
+
411
+ # ------------------------------------------------------------------
412
+ # Validate
413
+ # ------------------------------------------------------------------
414
+ # Validate
415
+ # ------------------------------------------------------------------
416
+
417
+ def validate(self) -> bool:
418
+ """
419
+ Verify AVL balance property and max_end invariant throughout the tree.
420
+
421
+ Complexity: O(n)
422
+
423
+ Returns:
424
+ True if tree is valid.
425
+
426
+ Raises:
427
+ RuntimeError: If any invariant is violated.
428
+ """
429
+ with self._lock:
430
+ self._validate_node(self._root)
431
+ return True
432
+
433
+ def _validate_node(self, node: _INode | None) -> tuple[int, int]:
434
+ """Returns (height, max_end) of subtree; raises on violation."""
435
+ if node is None:
436
+ return 0, -(2**62)
437
+ lh, lmax = self._validate_node(node.left)
438
+ rh, rmax = self._validate_node(node.right)
439
+ expected_height = 1 + max(lh, rh)
440
+ if node.height != expected_height:
441
+ raise RuntimeError(
442
+ f"Height mismatch at ({node.start},{node.end}): "
443
+ f"stored {node.height}, expected {expected_height}."
444
+ )
445
+ expected_max = max(node.end, lmax, rmax)
446
+ if node.max_end != expected_max:
447
+ raise RuntimeError(
448
+ f"max_end mismatch at ({node.start},{node.end}): "
449
+ f"stored {node.max_end}, expected {expected_max}."
450
+ )
451
+ bf = lh - rh
452
+ if abs(bf) > 1:
453
+ raise RuntimeError(
454
+ f"AVL balance violation at ({node.start},{node.end}): balance factor {bf}."
455
+ )
456
+ return expected_height, expected_max
457
+
458
+ # ------------------------------------------------------------------
459
+ # Clear / size / height
460
+ # ------------------------------------------------------------------
461
+
462
+ def clear(self) -> None:
463
+ """Remove all intervals from the tree. Complexity: O(1)."""
464
+ with self._lock:
465
+ self._root = None
466
+ self._size = 0
467
+
468
+ @property
469
+ def size(self) -> int:
470
+ """Number of intervals stored in the tree."""
471
+ return self._size
472
+
473
+ def height(self) -> int:
474
+ """
475
+ Return the height of the tree.
476
+
477
+ Complexity: O(1) [stored in root node]
478
+ """
479
+ with self._lock:
480
+ return _height(self._root)
481
+
482
+ @property
483
+ def max_endpoint(self) -> int | None:
484
+ """
485
+ Maximum endpoint across all intervals in the tree.
486
+
487
+ Returns None for an empty tree.
488
+
489
+ Complexity: O(1)
490
+ """
491
+ with self._lock:
492
+ return self._root.max_end if self._root is not None else None
493
+
494
+ # ------------------------------------------------------------------
495
+ # Dunder
496
+ # ------------------------------------------------------------------
497
+
498
+ def __len__(self) -> int:
499
+ """Return number of intervals. Complexity: O(1)."""
500
+ return self.size
501
+
502
+ def __contains__(self, interval: object) -> bool:
503
+ """
504
+ Return True if interval (start, end) is in the tree.
505
+
506
+ Complexity: O(log n)
507
+ """
508
+ with self._lock:
509
+ if not (isinstance(interval, tuple) and len(interval) == 2):
510
+ return False
511
+ start, end = interval # type: ignore[misc]
512
+ return self._find(self._root, start, end)
513
+
514
+ def _find(self, node: _INode | None, start: int, end: int) -> bool:
515
+ if node is None:
516
+ return False
517
+ if node.start == start and node.end == end:
518
+ return True
519
+ if (start, end) < (node.start, node.end):
520
+ return self._find(node.left, start, end)
521
+ return self._find(node.right, start, end)
522
+
523
+ def __iter__(self) -> Iterator[Interval]:
524
+ """
525
+ Iterate over all intervals in sorted (start, end) order.
526
+
527
+ Complexity: O(n)
528
+ """
529
+ with self._lock:
530
+ intervals: list[Interval] = []
531
+ _inorder(self._root, intervals)
532
+ result = list(intervals)
533
+ return iter(result)
534
+
535
+ def __repr__(self) -> str:
536
+ with self._lock:
537
+ return (
538
+ f"IntervalTree(size={self._size}, "
539
+ f"height={self.height()}, "
540
+ f"max_endpoint={self.max_endpoint})"
541
+ )