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,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
|
+
)
|