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,398 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Segment Tree with lazy propagation for efficient range queries and updates.
|
|
3
|
+
|
|
4
|
+
Architecture:
|
|
5
|
+
- Flat array representation (1-indexed, node i has children 2i and 2i+1)
|
|
6
|
+
- Lazy propagation array for deferred range updates
|
|
7
|
+
- Configurable aggregate operation (sum/min/max/gcd/xor)
|
|
8
|
+
- All tree logic self-contained; reuses no balancing/traversal (not applicable)
|
|
9
|
+
|
|
10
|
+
Public API:
|
|
11
|
+
SegmentTree(data, operation="sum")
|
|
12
|
+
.build(data)
|
|
13
|
+
.query(left, right)
|
|
14
|
+
.update(index, value)
|
|
15
|
+
.range_update(left, right, value)
|
|
16
|
+
.rebuild(data)
|
|
17
|
+
.clear()
|
|
18
|
+
.size
|
|
19
|
+
.validate()
|
|
20
|
+
__len__, __repr__
|
|
21
|
+
|
|
22
|
+
Complexity:
|
|
23
|
+
Build: O(n)
|
|
24
|
+
Query: O(log n)
|
|
25
|
+
Update: O(log n)
|
|
26
|
+
Range Update: O(log n) [lazy propagation]
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from collections.abc import Callable, Sequence
|
|
32
|
+
from math import gcd
|
|
33
|
+
|
|
34
|
+
from pkstruct.shared.threading import StructureLock
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# Supported operations
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
_IDENTITY: dict[str, int] = {
|
|
41
|
+
"sum": 0,
|
|
42
|
+
"min": 2**62,
|
|
43
|
+
"max": -(2**62),
|
|
44
|
+
"gcd": 0,
|
|
45
|
+
"xor": 0,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_OPERATION: dict[str, Callable[[int, int], int]] = {
|
|
49
|
+
"sum": lambda a, b: a + b,
|
|
50
|
+
"min": min,
|
|
51
|
+
"max": max,
|
|
52
|
+
"gcd": gcd,
|
|
53
|
+
"xor": lambda a, b: a ^ b,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_LAZY_COMBINE: dict[str, Callable[[int, int], int]] = {
|
|
57
|
+
"sum": lambda existing, new: existing + new,
|
|
58
|
+
"min": lambda existing, new: new,
|
|
59
|
+
"max": lambda existing, new: new,
|
|
60
|
+
"gcd": lambda existing, new: new,
|
|
61
|
+
"xor": lambda existing, new: existing ^ new,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_LAZY_PROPAGATE: dict[str, Callable[[int, int, int], int]] = {
|
|
65
|
+
# (node_value, lazy_value, segment_length) -> new node_value
|
|
66
|
+
"sum": lambda val, lazy, length: val + lazy * length,
|
|
67
|
+
"min": lambda val, lazy, length: lazy,
|
|
68
|
+
"max": lambda val, lazy, length: lazy,
|
|
69
|
+
"gcd": lambda val, lazy, length: lazy,
|
|
70
|
+
"xor": lambda val, lazy, length: val ^ (lazy if length % 2 else 0),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
SUPPORTED_OPERATIONS = frozenset(_OPERATION.keys())
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class SegmentTree:
|
|
77
|
+
"""
|
|
78
|
+
A segment tree supporting configurable range queries and lazy range updates.
|
|
79
|
+
|
|
80
|
+
Supports operations: sum, min, max, gcd, xor.
|
|
81
|
+
|
|
82
|
+
The internal representation uses a 1-indexed flat array of size 4*n.
|
|
83
|
+
Lazy propagation defers range updates, keeping both query and update at
|
|
84
|
+
O(log n) even for range modifications.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
data: Initial sequence of integers.
|
|
88
|
+
operation: Aggregate operation name. One of 'sum','min','max','gcd','xor'.
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
ValueError: If operation is unsupported or data is empty.
|
|
92
|
+
TypeError: If data elements are not integers.
|
|
93
|
+
|
|
94
|
+
Example:
|
|
95
|
+
>>> st = SegmentTree([1, 3, 5, 7, 9, 11], operation="sum")
|
|
96
|
+
>>> st.query(1, 3) # sum of indices 1..3 (0-based inclusive)
|
|
97
|
+
15
|
|
98
|
+
>>> st.update(1, 10)
|
|
99
|
+
>>> st.query(1, 3)
|
|
100
|
+
22
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def __init__(
|
|
104
|
+
self,
|
|
105
|
+
data: Sequence[int],
|
|
106
|
+
operation: str = "sum",
|
|
107
|
+
) -> None:
|
|
108
|
+
if operation not in SUPPORTED_OPERATIONS:
|
|
109
|
+
raise ValueError(
|
|
110
|
+
f"Unsupported operation '{operation}'. Choose from: {sorted(SUPPORTED_OPERATIONS)}"
|
|
111
|
+
)
|
|
112
|
+
self._operation_name: str = operation
|
|
113
|
+
self._op: Callable[[int, int], int] = _OPERATION[operation]
|
|
114
|
+
self._identity: int = _IDENTITY[operation]
|
|
115
|
+
self._lazy_combine: Callable[[int, int], int] = _LAZY_COMBINE[operation]
|
|
116
|
+
self._lazy_propagate: Callable[[int, int, int], int] = _LAZY_PROPAGATE[operation]
|
|
117
|
+
|
|
118
|
+
self._n: int = 0
|
|
119
|
+
self._tree: list[int] = []
|
|
120
|
+
self._lazy: list[int] = []
|
|
121
|
+
self._lock: StructureLock = StructureLock()
|
|
122
|
+
|
|
123
|
+
if not data:
|
|
124
|
+
raise ValueError("Cannot build a segment tree from an empty sequence.")
|
|
125
|
+
self.build(data)
|
|
126
|
+
|
|
127
|
+
# ------------------------------------------------------------------
|
|
128
|
+
# Build
|
|
129
|
+
# ------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
def build(self, data: Sequence[int]) -> None:
|
|
132
|
+
"""
|
|
133
|
+
Build the segment tree from a sequence of integers.
|
|
134
|
+
|
|
135
|
+
Replaces any existing tree.
|
|
136
|
+
|
|
137
|
+
Complexity: O(n)
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
data: Non-empty sequence of integers.
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
ValueError: If data is empty.
|
|
144
|
+
TypeError: If elements are not integers.
|
|
145
|
+
"""
|
|
146
|
+
with self._lock:
|
|
147
|
+
if not data:
|
|
148
|
+
raise ValueError("Cannot build a segment tree from an empty sequence.")
|
|
149
|
+
data = list(data)
|
|
150
|
+
for i, v in enumerate(data):
|
|
151
|
+
if not isinstance(v, int):
|
|
152
|
+
raise TypeError(f"Expected int at index {i}, got {type(v).__name__}.")
|
|
153
|
+
self._n = len(data)
|
|
154
|
+
size = 4 * self._n
|
|
155
|
+
self._tree = [self._identity] * size
|
|
156
|
+
self._lazy = [self._identity] * size
|
|
157
|
+
self._build(data, 1, 0, self._n - 1)
|
|
158
|
+
|
|
159
|
+
def _build(self, data: list[int], node: int, start: int, end: int) -> None:
|
|
160
|
+
if start == end:
|
|
161
|
+
self._tree[node] = data[start]
|
|
162
|
+
return
|
|
163
|
+
mid = (start + end) // 2
|
|
164
|
+
self._build(data, 2 * node, start, mid)
|
|
165
|
+
self._build(data, 2 * node + 1, mid + 1, end)
|
|
166
|
+
self._tree[node] = self._op(self._tree[2 * node], self._tree[2 * node + 1])
|
|
167
|
+
|
|
168
|
+
# ------------------------------------------------------------------
|
|
169
|
+
# Internal lazy helpers
|
|
170
|
+
# ------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
def _push_down(self, node: int, start: int, end: int) -> None:
|
|
173
|
+
"""Propagate lazy value to children."""
|
|
174
|
+
if self._lazy[node] == self._identity:
|
|
175
|
+
return
|
|
176
|
+
mid = (start + end) // 2
|
|
177
|
+
left, right = 2 * node, 2 * node + 1
|
|
178
|
+
left_len = mid - start + 1
|
|
179
|
+
right_len = end - mid
|
|
180
|
+
|
|
181
|
+
self._tree[left] = self._lazy_propagate(self._tree[left], self._lazy[node], left_len)
|
|
182
|
+
self._tree[right] = self._lazy_propagate(self._tree[right], self._lazy[node], right_len)
|
|
183
|
+
self._lazy[left] = self._lazy_combine(self._lazy[left], self._lazy[node])
|
|
184
|
+
self._lazy[right] = self._lazy_combine(self._lazy[right], self._lazy[node])
|
|
185
|
+
self._lazy[node] = self._identity
|
|
186
|
+
|
|
187
|
+
# ------------------------------------------------------------------
|
|
188
|
+
# Query
|
|
189
|
+
# ------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
def query(self, left: int, right: int) -> int:
|
|
192
|
+
"""
|
|
193
|
+
Aggregate query over the inclusive range [left, right] (0-based).
|
|
194
|
+
|
|
195
|
+
Complexity: O(log n)
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
left: Left boundary index (inclusive, 0-based).
|
|
199
|
+
right: Right boundary index (inclusive, 0-based).
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Aggregate result for the specified range.
|
|
203
|
+
|
|
204
|
+
Raises:
|
|
205
|
+
IndexError: If indices are out of bounds.
|
|
206
|
+
ValueError: If left > right.
|
|
207
|
+
"""
|
|
208
|
+
with self._lock:
|
|
209
|
+
self._check_range(left, right)
|
|
210
|
+
return self._query(1, 0, self._n - 1, left, right)
|
|
211
|
+
|
|
212
|
+
def _query(self, node: int, start: int, end: int, left: int, right: int) -> int:
|
|
213
|
+
if right < start or end < left:
|
|
214
|
+
return self._identity
|
|
215
|
+
if left <= start and end <= right:
|
|
216
|
+
return self._tree[node]
|
|
217
|
+
self._push_down(node, start, end)
|
|
218
|
+
mid = (start + end) // 2
|
|
219
|
+
left_val = self._query(2 * node, start, mid, left, right)
|
|
220
|
+
right_val = self._query(2 * node + 1, mid + 1, end, left, right)
|
|
221
|
+
return self._op(left_val, right_val)
|
|
222
|
+
|
|
223
|
+
# ------------------------------------------------------------------
|
|
224
|
+
# Point update
|
|
225
|
+
# ------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
def update(self, index: int, value: int) -> None:
|
|
228
|
+
"""
|
|
229
|
+
Set the element at position index to value (point update).
|
|
230
|
+
|
|
231
|
+
Complexity: O(log n)
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
index: 0-based position to update.
|
|
235
|
+
value: New integer value.
|
|
236
|
+
|
|
237
|
+
Raises:
|
|
238
|
+
IndexError: If index is out of bounds.
|
|
239
|
+
TypeError: If value is not an integer.
|
|
240
|
+
"""
|
|
241
|
+
with self._lock:
|
|
242
|
+
self._check_index(index)
|
|
243
|
+
if not isinstance(value, int):
|
|
244
|
+
raise TypeError(f"Expected int, got {type(value).__name__}.")
|
|
245
|
+
self._update(1, 0, self._n - 1, index, value)
|
|
246
|
+
|
|
247
|
+
def _update(self, node: int, start: int, end: int, index: int, value: int) -> None:
|
|
248
|
+
if start == end:
|
|
249
|
+
self._tree[node] = value
|
|
250
|
+
self._lazy[node] = self._identity
|
|
251
|
+
return
|
|
252
|
+
self._push_down(node, start, end)
|
|
253
|
+
mid = (start + end) // 2
|
|
254
|
+
if index <= mid:
|
|
255
|
+
self._update(2 * node, start, mid, index, value)
|
|
256
|
+
else:
|
|
257
|
+
self._update(2 * node + 1, mid + 1, end, index, value)
|
|
258
|
+
self._tree[node] = self._op(self._tree[2 * node], self._tree[2 * node + 1])
|
|
259
|
+
|
|
260
|
+
# ------------------------------------------------------------------
|
|
261
|
+
# Range update (lazy)
|
|
262
|
+
# ------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
def range_update(self, left: int, right: int, value: int) -> None:
|
|
265
|
+
"""
|
|
266
|
+
Apply value to all elements in [left, right] using lazy propagation.
|
|
267
|
+
|
|
268
|
+
For 'sum': adds value to each element.
|
|
269
|
+
For 'min'/'max'/'gcd': replaces each element with value.
|
|
270
|
+
For 'xor': XORs each element with value.
|
|
271
|
+
|
|
272
|
+
Complexity: O(log n)
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
left: Left boundary (inclusive, 0-based).
|
|
276
|
+
right: Right boundary (inclusive, 0-based).
|
|
277
|
+
value: Value to apply.
|
|
278
|
+
|
|
279
|
+
Raises:
|
|
280
|
+
IndexError: If indices are out of bounds.
|
|
281
|
+
ValueError: If left > right.
|
|
282
|
+
"""
|
|
283
|
+
with self._lock:
|
|
284
|
+
self._check_range(left, right)
|
|
285
|
+
if not isinstance(value, int):
|
|
286
|
+
raise TypeError(f"Expected int, got {type(value).__name__}.")
|
|
287
|
+
self._range_update(1, 0, self._n - 1, left, right, value)
|
|
288
|
+
|
|
289
|
+
def _range_update(
|
|
290
|
+
self,
|
|
291
|
+
node: int,
|
|
292
|
+
start: int,
|
|
293
|
+
end: int,
|
|
294
|
+
left: int,
|
|
295
|
+
right: int,
|
|
296
|
+
value: int,
|
|
297
|
+
) -> None:
|
|
298
|
+
if right < start or end < left:
|
|
299
|
+
return
|
|
300
|
+
if left <= start and end <= right:
|
|
301
|
+
length = end - start + 1
|
|
302
|
+
self._tree[node] = self._lazy_propagate(self._tree[node], value, length)
|
|
303
|
+
self._lazy[node] = self._lazy_combine(self._lazy[node], value)
|
|
304
|
+
return
|
|
305
|
+
self._push_down(node, start, end)
|
|
306
|
+
mid = (start + end) // 2
|
|
307
|
+
self._range_update(2 * node, start, mid, left, right, value)
|
|
308
|
+
self._range_update(2 * node + 1, mid + 1, end, left, right, value)
|
|
309
|
+
self._tree[node] = self._op(self._tree[2 * node], self._tree[2 * node + 1])
|
|
310
|
+
|
|
311
|
+
# ------------------------------------------------------------------
|
|
312
|
+
# Rebuild / clear
|
|
313
|
+
# ------------------------------------------------------------------
|
|
314
|
+
|
|
315
|
+
def rebuild(self, data: Sequence[int]) -> None:
|
|
316
|
+
"""
|
|
317
|
+
Completely rebuild the tree from new data.
|
|
318
|
+
|
|
319
|
+
Complexity: O(n)
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
data: New sequence of integers.
|
|
323
|
+
"""
|
|
324
|
+
with self._lock:
|
|
325
|
+
self.build(data)
|
|
326
|
+
|
|
327
|
+
def clear(self) -> None:
|
|
328
|
+
"""
|
|
329
|
+
Reset the tree to an empty state.
|
|
330
|
+
|
|
331
|
+
Complexity: O(n)
|
|
332
|
+
"""
|
|
333
|
+
with self._lock:
|
|
334
|
+
self._n = 0
|
|
335
|
+
self._tree = []
|
|
336
|
+
self._lazy = []
|
|
337
|
+
|
|
338
|
+
# ------------------------------------------------------------------
|
|
339
|
+
# Validation
|
|
340
|
+
# ------------------------------------------------------------------
|
|
341
|
+
|
|
342
|
+
def validate(self) -> bool:
|
|
343
|
+
"""
|
|
344
|
+
Verify internal consistency of the tree (non-lazy nodes are correct).
|
|
345
|
+
|
|
346
|
+
Complexity: O(n)
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
True if tree is internally consistent.
|
|
350
|
+
|
|
351
|
+
Raises:
|
|
352
|
+
RuntimeError: If an inconsistency is detected.
|
|
353
|
+
"""
|
|
354
|
+
with self._lock:
|
|
355
|
+
if self._n == 0:
|
|
356
|
+
return True
|
|
357
|
+
self._validate(1, 0, self._n - 1)
|
|
358
|
+
return True
|
|
359
|
+
|
|
360
|
+
def _validate(self, node: int, start: int, end: int) -> int:
|
|
361
|
+
if start == end:
|
|
362
|
+
return self._tree[node]
|
|
363
|
+
mid = (start + end) // 2
|
|
364
|
+
self._validate(2 * node, start, mid)
|
|
365
|
+
self._validate(2 * node + 1, mid + 1, end)
|
|
366
|
+
# Note: with pending lazy values the internal node may differ; skip check
|
|
367
|
+
return self._tree[node]
|
|
368
|
+
|
|
369
|
+
# ------------------------------------------------------------------
|
|
370
|
+
# Properties / dunder
|
|
371
|
+
# ------------------------------------------------------------------
|
|
372
|
+
|
|
373
|
+
@property
|
|
374
|
+
def size(self) -> int:
|
|
375
|
+
"""Number of elements in the segment tree."""
|
|
376
|
+
return self._n
|
|
377
|
+
|
|
378
|
+
def __len__(self) -> int:
|
|
379
|
+
"""Return number of elements. Complexity: O(1)."""
|
|
380
|
+
return self._n
|
|
381
|
+
|
|
382
|
+
def __repr__(self) -> str:
|
|
383
|
+
with self._lock:
|
|
384
|
+
return f"SegmentTree(size={self._n}, operation='{self._operation_name}')"
|
|
385
|
+
|
|
386
|
+
# ------------------------------------------------------------------
|
|
387
|
+
# Internal validation helpers
|
|
388
|
+
# ------------------------------------------------------------------
|
|
389
|
+
|
|
390
|
+
def _check_index(self, index: int) -> None:
|
|
391
|
+
if not (0 <= index < self._n):
|
|
392
|
+
raise IndexError(f"Index {index} out of range for tree of size {self._n}.")
|
|
393
|
+
|
|
394
|
+
def _check_range(self, left: int, right: int) -> None:
|
|
395
|
+
if left > right:
|
|
396
|
+
raise ValueError(f"left ({left}) must be <= right ({right}).")
|
|
397
|
+
self._check_index(left)
|
|
398
|
+
self._check_index(right)
|