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,608 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from collections.abc import Callable, Iterator
|
|
5
|
+
from typing import Any, Generic, TypeVar, cast
|
|
6
|
+
|
|
7
|
+
from pkstruct.linear.exceptions import (
|
|
8
|
+
EmptyStructureError,
|
|
9
|
+
IndexOutOfRangeError,
|
|
10
|
+
ValidationError,
|
|
11
|
+
ValueNotFoundError,
|
|
12
|
+
)
|
|
13
|
+
from pkstruct.shared.debugging import DebugTracer
|
|
14
|
+
from pkstruct.shared.serializers import deserialize_from_json, serialize_to_json
|
|
15
|
+
from pkstruct.shared.threading import StructureLock
|
|
16
|
+
from pkstruct.shared.validators import validate_index, validate_range
|
|
17
|
+
|
|
18
|
+
T = TypeVar("T")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _LinkedListBase(Generic[T], ABC):
|
|
22
|
+
__slots__ = ("_head", "_tail", "_size", "_lock", "_tracer")
|
|
23
|
+
|
|
24
|
+
def __init__(self) -> None:
|
|
25
|
+
self._head: Any = None
|
|
26
|
+
self._tail: Any = None
|
|
27
|
+
self._size: int = 0
|
|
28
|
+
self._lock: StructureLock = StructureLock()
|
|
29
|
+
self._tracer: DebugTracer = DebugTracer()
|
|
30
|
+
|
|
31
|
+
# ------------------------------------------------------------------ #
|
|
32
|
+
# Abstract node operations #
|
|
33
|
+
# ------------------------------------------------------------------ #
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def _append(self, value: T) -> None: ...
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def _prepend(self, value: T) -> None: ...
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def _node_at(self, index: int) -> Any: ...
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def _find_node(self, value: object) -> Any | None: ...
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def _remove_node(self, node: Any) -> T: ...
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def _insert_before_node(self, node: Any, value: T) -> None: ...
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def _insert_after_node(self, node: Any, value: T) -> None: ...
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def _to_list_unsafe(self) -> list[T]: ...
|
|
58
|
+
|
|
59
|
+
# ------------------------------------------------------------------ #
|
|
60
|
+
# Construction helpers #
|
|
61
|
+
# ------------------------------------------------------------------ #
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def create(cls) -> _LinkedListBase[T]:
|
|
65
|
+
return cls()
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def from_list(cls, items: list[T]) -> _LinkedListBase[T]:
|
|
69
|
+
if not isinstance(items, list):
|
|
70
|
+
raise ValidationError(f"'items' must be a list, got {type(items).__name__!r}.")
|
|
71
|
+
obj: _LinkedListBase[T] = cls()
|
|
72
|
+
for item in items:
|
|
73
|
+
obj._append(item)
|
|
74
|
+
return obj
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def from_json(cls, json_str: str) -> _LinkedListBase[T]:
|
|
78
|
+
items: list[T] = deserialize_from_json(json_str)
|
|
79
|
+
return cls.from_list(items)
|
|
80
|
+
|
|
81
|
+
def to_json(self) -> str:
|
|
82
|
+
return serialize_to_json(self._to_list_unsafe())
|
|
83
|
+
|
|
84
|
+
def copy(self) -> _LinkedListBase[T]:
|
|
85
|
+
with self._lock:
|
|
86
|
+
return self.from_list(self._to_list_unsafe())
|
|
87
|
+
|
|
88
|
+
# ------------------------------------------------------------------ #
|
|
89
|
+
# Insertion #
|
|
90
|
+
# ------------------------------------------------------------------ #
|
|
91
|
+
|
|
92
|
+
def insert(
|
|
93
|
+
self,
|
|
94
|
+
value: T,
|
|
95
|
+
position: int | None = None,
|
|
96
|
+
before: T | None = None,
|
|
97
|
+
after: T | None = None,
|
|
98
|
+
) -> None:
|
|
99
|
+
provided = sum(x is not None for x in (position, before, after))
|
|
100
|
+
if provided > 1:
|
|
101
|
+
raise ValidationError("Provide at most one of 'position', 'before', or 'after'.")
|
|
102
|
+
with self._lock:
|
|
103
|
+
self._tracer.record("insert", value=value, position=position, before=before, after=after)
|
|
104
|
+
|
|
105
|
+
if before is not None:
|
|
106
|
+
target = self._find_node(before)
|
|
107
|
+
if target is None:
|
|
108
|
+
raise ValueNotFoundError(before)
|
|
109
|
+
if target is self._head:
|
|
110
|
+
self._prepend(value)
|
|
111
|
+
else:
|
|
112
|
+
self._insert_before_node(target, value)
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
if after is not None:
|
|
116
|
+
target = self._find_node(after)
|
|
117
|
+
if target is None:
|
|
118
|
+
raise ValueNotFoundError(after)
|
|
119
|
+
self._insert_after_node(target, value)
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
if position is None:
|
|
123
|
+
self._append(value)
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
if position < 0:
|
|
127
|
+
raise IndexOutOfRangeError(position, self._size)
|
|
128
|
+
if position == 0:
|
|
129
|
+
self._prepend(value)
|
|
130
|
+
return
|
|
131
|
+
if position > self._size:
|
|
132
|
+
raise IndexOutOfRangeError(position, self._size)
|
|
133
|
+
if position == self._size:
|
|
134
|
+
self._append(value)
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
target = self._node_at(position)
|
|
138
|
+
self._insert_before_node(target, value)
|
|
139
|
+
|
|
140
|
+
def extend(self, values: list[T] | _LinkedListBase[T]) -> None:
|
|
141
|
+
if isinstance(values, _LinkedListBase):
|
|
142
|
+
items: list[T] = values._to_list_unsafe()
|
|
143
|
+
elif isinstance(values, list):
|
|
144
|
+
items = values
|
|
145
|
+
else:
|
|
146
|
+
raise ValidationError(
|
|
147
|
+
f"'values' must be a list or linked list, got {type(values).__name__!r}."
|
|
148
|
+
)
|
|
149
|
+
with self._lock:
|
|
150
|
+
for item in items:
|
|
151
|
+
self._append(item)
|
|
152
|
+
|
|
153
|
+
def merge(self, *others: _LinkedListBase[T]) -> None:
|
|
154
|
+
with self._lock:
|
|
155
|
+
for other in others:
|
|
156
|
+
with other._lock:
|
|
157
|
+
values = other._to_list_unsafe()
|
|
158
|
+
for v in values:
|
|
159
|
+
self._append(v)
|
|
160
|
+
|
|
161
|
+
# ------------------------------------------------------------------ #
|
|
162
|
+
# Deletion #
|
|
163
|
+
# ------------------------------------------------------------------ #
|
|
164
|
+
|
|
165
|
+
def delete(
|
|
166
|
+
self,
|
|
167
|
+
value: T | None = None,
|
|
168
|
+
position: int | None = None,
|
|
169
|
+
rng: tuple[int, int] | None = None,
|
|
170
|
+
) -> T | list[T] | None:
|
|
171
|
+
provided = sum(x is not None for x in (value, position, rng))
|
|
172
|
+
if provided == 0:
|
|
173
|
+
raise ValidationError("Provide one of 'value', 'position', or 'rng'.")
|
|
174
|
+
if provided > 1:
|
|
175
|
+
raise ValidationError("Provide only one of 'value', 'position', or 'rng'.")
|
|
176
|
+
with self._lock:
|
|
177
|
+
if self._size == 0:
|
|
178
|
+
raise EmptyStructureError("delete")
|
|
179
|
+
self._tracer.record("delete", value=value, position=position, rng=rng)
|
|
180
|
+
|
|
181
|
+
if value is not None:
|
|
182
|
+
node = self._find_node(value)
|
|
183
|
+
if node is None:
|
|
184
|
+
raise ValueNotFoundError(value)
|
|
185
|
+
return self._remove_node(node)
|
|
186
|
+
|
|
187
|
+
if position is not None:
|
|
188
|
+
normalised = position if position >= 0 else position + self._size
|
|
189
|
+
if not (0 <= normalised < self._size):
|
|
190
|
+
raise IndexOutOfRangeError(position, self._size)
|
|
191
|
+
node = self._node_at(normalised)
|
|
192
|
+
return self._remove_node(node)
|
|
193
|
+
|
|
194
|
+
if rng is None:
|
|
195
|
+
raise RuntimeError("internal invariant violated: rng should not be None in the range-delete branch")
|
|
196
|
+
start, end = rng
|
|
197
|
+
validate_range(start, end, self._size)
|
|
198
|
+
removed: list[T] = []
|
|
199
|
+
for _ in range(end - start + 1):
|
|
200
|
+
node = self._node_at(start)
|
|
201
|
+
removed.append(self._remove_node(node))
|
|
202
|
+
return removed
|
|
203
|
+
|
|
204
|
+
def clear(self) -> None:
|
|
205
|
+
with self._lock:
|
|
206
|
+
self._head = None
|
|
207
|
+
self._tail = None
|
|
208
|
+
self._size = 0
|
|
209
|
+
self._tracer.record("clear")
|
|
210
|
+
|
|
211
|
+
# ------------------------------------------------------------------ #
|
|
212
|
+
# Access #
|
|
213
|
+
# ------------------------------------------------------------------ #
|
|
214
|
+
|
|
215
|
+
def get(self, position: int, from_end: bool = False) -> T:
|
|
216
|
+
with self._lock:
|
|
217
|
+
if self._size == 0:
|
|
218
|
+
raise EmptyStructureError("get")
|
|
219
|
+
if from_end:
|
|
220
|
+
if position < 0:
|
|
221
|
+
raise IndexOutOfRangeError(position, self._size)
|
|
222
|
+
validate_index(position, self._size)
|
|
223
|
+
idx = self._size - 1 - position
|
|
224
|
+
return cast(T, self._node_at(idx).value)
|
|
225
|
+
if position < 0:
|
|
226
|
+
raise IndexOutOfRangeError(position, self._size)
|
|
227
|
+
validate_index(position, self._size)
|
|
228
|
+
return cast(T, self._node_at(position).value)
|
|
229
|
+
|
|
230
|
+
def to_list(self) -> list[T]:
|
|
231
|
+
with self._lock:
|
|
232
|
+
return self._to_list_unsafe()
|
|
233
|
+
|
|
234
|
+
def size(self) -> int:
|
|
235
|
+
with self._lock:
|
|
236
|
+
return self._size
|
|
237
|
+
|
|
238
|
+
def is_empty(self) -> bool:
|
|
239
|
+
with self._lock:
|
|
240
|
+
return self._size == 0
|
|
241
|
+
|
|
242
|
+
def count(self, value: T) -> int:
|
|
243
|
+
with self._lock:
|
|
244
|
+
n = 0
|
|
245
|
+
node = self._head
|
|
246
|
+
for _ in range(self._size):
|
|
247
|
+
if node.value == value:
|
|
248
|
+
n += 1
|
|
249
|
+
node = node.next
|
|
250
|
+
return n
|
|
251
|
+
|
|
252
|
+
# ------------------------------------------------------------------ #
|
|
253
|
+
# Search #
|
|
254
|
+
# ------------------------------------------------------------------ #
|
|
255
|
+
|
|
256
|
+
def index(self, value: T) -> int:
|
|
257
|
+
with self._lock:
|
|
258
|
+
node = self._head
|
|
259
|
+
for i in range(self._size):
|
|
260
|
+
if node.value == value:
|
|
261
|
+
return i
|
|
262
|
+
node = node.next
|
|
263
|
+
raise ValueNotFoundError(value)
|
|
264
|
+
|
|
265
|
+
# ------------------------------------------------------------------ #
|
|
266
|
+
# Replacement #
|
|
267
|
+
# ------------------------------------------------------------------ #
|
|
268
|
+
|
|
269
|
+
def replace(
|
|
270
|
+
self,
|
|
271
|
+
new_value: T,
|
|
272
|
+
old_value: T | None = None,
|
|
273
|
+
position: int | None = None,
|
|
274
|
+
replace_all: bool = False,
|
|
275
|
+
) -> int:
|
|
276
|
+
if new_value is None:
|
|
277
|
+
raise ValidationError("'new_value' is required.")
|
|
278
|
+
with self._lock:
|
|
279
|
+
self._tracer.record(
|
|
280
|
+
"replace", old_value=old_value, new_value=new_value, position=position
|
|
281
|
+
)
|
|
282
|
+
if position is not None:
|
|
283
|
+
idx = position if position >= 0 else position + self._size
|
|
284
|
+
validate_index(idx, self._size)
|
|
285
|
+
self._node_at(idx).value = new_value
|
|
286
|
+
return 1
|
|
287
|
+
if old_value is None:
|
|
288
|
+
raise ValidationError("Provide 'old_value' or 'position'.")
|
|
289
|
+
count = 0
|
|
290
|
+
node = self._head
|
|
291
|
+
for _ in range(self._size):
|
|
292
|
+
if node.value == old_value:
|
|
293
|
+
node.value = new_value
|
|
294
|
+
count += 1
|
|
295
|
+
if not replace_all:
|
|
296
|
+
break
|
|
297
|
+
node = node.next
|
|
298
|
+
if count == 0:
|
|
299
|
+
raise ValueNotFoundError(old_value)
|
|
300
|
+
return count
|
|
301
|
+
|
|
302
|
+
# ------------------------------------------------------------------ #
|
|
303
|
+
# Reverse & Rotation #
|
|
304
|
+
# ------------------------------------------------------------------ #
|
|
305
|
+
|
|
306
|
+
def reverse(self, start: int | None = None, end: int | None = None) -> None:
|
|
307
|
+
with self._lock:
|
|
308
|
+
if self._size == 0:
|
|
309
|
+
raise EmptyStructureError("reverse an empty list")
|
|
310
|
+
lo = 0 if start is None else start
|
|
311
|
+
hi = self._size - 1 if end is None else end
|
|
312
|
+
if lo >= hi:
|
|
313
|
+
return
|
|
314
|
+
validate_range(lo, hi, self._size)
|
|
315
|
+
self._tracer.record("reverse", start=lo, end=hi)
|
|
316
|
+
nodes: list[Any] = []
|
|
317
|
+
node = self._node_at(lo)
|
|
318
|
+
for _ in range(hi - lo + 1):
|
|
319
|
+
nodes.append(node)
|
|
320
|
+
node = node.next
|
|
321
|
+
left, right = 0, len(nodes) - 1
|
|
322
|
+
while left < right:
|
|
323
|
+
nodes[left].value, nodes[right].value = nodes[right].value, nodes[left].value
|
|
324
|
+
left += 1
|
|
325
|
+
right -= 1
|
|
326
|
+
|
|
327
|
+
def rotate(
|
|
328
|
+
self,
|
|
329
|
+
shift: int = 1,
|
|
330
|
+
start: int | None = None,
|
|
331
|
+
end: int | None = None,
|
|
332
|
+
direction: bool = True,
|
|
333
|
+
) -> None:
|
|
334
|
+
with self._lock:
|
|
335
|
+
if self._size == 0:
|
|
336
|
+
raise EmptyStructureError("rotate an empty list")
|
|
337
|
+
|
|
338
|
+
actual_shift = shift
|
|
339
|
+
if shift < 0:
|
|
340
|
+
actual_shift = abs(shift)
|
|
341
|
+
direction = False
|
|
342
|
+
|
|
343
|
+
actual_start = 0 if start is None else start
|
|
344
|
+
actual_end = self._size - 1 if end is None else end
|
|
345
|
+
|
|
346
|
+
if actual_start < 0:
|
|
347
|
+
actual_start = self._size + actual_start
|
|
348
|
+
if actual_end < 0:
|
|
349
|
+
actual_end = self._size + actual_end
|
|
350
|
+
|
|
351
|
+
validate_range(actual_start, actual_end, self._size)
|
|
352
|
+
|
|
353
|
+
length = actual_end - actual_start + 1
|
|
354
|
+
if length <= 1:
|
|
355
|
+
return
|
|
356
|
+
|
|
357
|
+
effective_shift = actual_shift % length
|
|
358
|
+
if effective_shift == 0:
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
if not direction:
|
|
362
|
+
effective_shift = length - effective_shift
|
|
363
|
+
|
|
364
|
+
self._tracer.record(
|
|
365
|
+
"rotate", shift=actual_shift, start=actual_start, end=actual_end, direction=direction
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
if actual_start == 0 and actual_end == self._size - 1:
|
|
369
|
+
self._rotate_full_list(effective_shift)
|
|
370
|
+
else:
|
|
371
|
+
self._rotate_subrange(actual_start, actual_end, effective_shift)
|
|
372
|
+
|
|
373
|
+
def _rotate_full_list(self, shift: int) -> None:
|
|
374
|
+
if shift == 0 or self._size <= 1:
|
|
375
|
+
return
|
|
376
|
+
values = self._to_list_unsafe()
|
|
377
|
+
rotated = values[-shift:] + values[:-shift]
|
|
378
|
+
node = self._head
|
|
379
|
+
for v in rotated:
|
|
380
|
+
node.value = v
|
|
381
|
+
node = node.next
|
|
382
|
+
|
|
383
|
+
def _rotate_subrange(self, start: int, end: int, shift: int) -> None:
|
|
384
|
+
nodes: list[Any] = []
|
|
385
|
+
node = self._node_at(start)
|
|
386
|
+
for _ in range(end - start + 1):
|
|
387
|
+
nodes.append(node)
|
|
388
|
+
node = node.next
|
|
389
|
+
values = [n.value for n in nodes]
|
|
390
|
+
rotated = values[-shift:] + values[:-shift]
|
|
391
|
+
for n, v in zip(nodes, rotated, strict=True):
|
|
392
|
+
n.value = v
|
|
393
|
+
|
|
394
|
+
# ------------------------------------------------------------------ #
|
|
395
|
+
# Swapping #
|
|
396
|
+
# ------------------------------------------------------------------ #
|
|
397
|
+
|
|
398
|
+
def swap(
|
|
399
|
+
self,
|
|
400
|
+
value1: T | None = None,
|
|
401
|
+
value2: T | None = None,
|
|
402
|
+
pos1: int | None = None,
|
|
403
|
+
pos2: int | None = None,
|
|
404
|
+
pairwise: bool = False,
|
|
405
|
+
) -> None:
|
|
406
|
+
with self._lock:
|
|
407
|
+
self._tracer.record(
|
|
408
|
+
"swap", value1=value1, value2=value2, pos1=pos1, pos2=pos2, pairwise=pairwise
|
|
409
|
+
)
|
|
410
|
+
if pairwise:
|
|
411
|
+
node = self._head
|
|
412
|
+
for _ in range(self._size // 2):
|
|
413
|
+
node.value, node.next.value = node.next.value, node.value
|
|
414
|
+
node = node.next.next
|
|
415
|
+
return
|
|
416
|
+
if self._size == 0:
|
|
417
|
+
raise EmptyStructureError("swap")
|
|
418
|
+
if value1 is not None and value2 is not None:
|
|
419
|
+
n1 = self._find_node(value1)
|
|
420
|
+
n2 = self._find_node(value2)
|
|
421
|
+
if n1 is None:
|
|
422
|
+
raise ValueNotFoundError(value1)
|
|
423
|
+
if n2 is None:
|
|
424
|
+
raise ValueNotFoundError(value2)
|
|
425
|
+
n1.value, n2.value = n2.value, n1.value
|
|
426
|
+
return
|
|
427
|
+
if pos1 is not None and pos2 is not None:
|
|
428
|
+
i1 = pos1 if pos1 >= 0 else pos1 + self._size
|
|
429
|
+
i2 = pos2 if pos2 >= 0 else pos2 + self._size
|
|
430
|
+
validate_index(i1, self._size)
|
|
431
|
+
validate_index(i2, self._size)
|
|
432
|
+
n1 = self._node_at(i1)
|
|
433
|
+
n2 = self._node_at(i2)
|
|
434
|
+
n1.value, n2.value = n2.value, n1.value
|
|
435
|
+
return
|
|
436
|
+
raise ValidationError("Provide (value1, value2), (pos1, pos2), or pairwise=True.")
|
|
437
|
+
|
|
438
|
+
# ------------------------------------------------------------------ #
|
|
439
|
+
# Sort & Partition #
|
|
440
|
+
# ------------------------------------------------------------------ #
|
|
441
|
+
|
|
442
|
+
def sort(
|
|
443
|
+
self,
|
|
444
|
+
reverse: bool = False,
|
|
445
|
+
key: Callable[[T], Any] | None = None,
|
|
446
|
+
) -> None:
|
|
447
|
+
with self._lock:
|
|
448
|
+
if self._size <= 1:
|
|
449
|
+
return
|
|
450
|
+
self._tracer.record("sort", reverse=reverse)
|
|
451
|
+
values = self._to_list_unsafe()
|
|
452
|
+
values.sort(key=key, reverse=reverse)
|
|
453
|
+
node = self._head
|
|
454
|
+
for v in values:
|
|
455
|
+
node.value = v
|
|
456
|
+
node = node.next
|
|
457
|
+
|
|
458
|
+
def partition(self, predicate_or_pivot: Callable[[T], bool] | T) -> None:
|
|
459
|
+
with self._lock:
|
|
460
|
+
if self._size <= 1:
|
|
461
|
+
return
|
|
462
|
+
if callable(predicate_or_pivot):
|
|
463
|
+
pred = predicate_or_pivot
|
|
464
|
+
else:
|
|
465
|
+
pivot = predicate_or_pivot
|
|
466
|
+
def pred(x: T) -> bool:
|
|
467
|
+
return cast(bool, x < pivot) # type: ignore[operator]
|
|
468
|
+
values = self._to_list_unsafe()
|
|
469
|
+
left = [v for v in values if pred(v)]
|
|
470
|
+
right = [v for v in values if not pred(v)]
|
|
471
|
+
new_values = left + right
|
|
472
|
+
node = self._head
|
|
473
|
+
for v in new_values:
|
|
474
|
+
node.value = v
|
|
475
|
+
node = node.next
|
|
476
|
+
|
|
477
|
+
# ------------------------------------------------------------------ #
|
|
478
|
+
# Interview problems #
|
|
479
|
+
# ------------------------------------------------------------------ #
|
|
480
|
+
|
|
481
|
+
def detect_cycle(self, return_start: bool = False) -> bool | tuple[bool, Any]:
|
|
482
|
+
with self._lock:
|
|
483
|
+
slow = self._head
|
|
484
|
+
fast = self._head
|
|
485
|
+
while fast is not None and fast.next is not None:
|
|
486
|
+
slow = slow.next
|
|
487
|
+
fast = fast.next.next
|
|
488
|
+
if slow is fast:
|
|
489
|
+
if not return_start:
|
|
490
|
+
return True
|
|
491
|
+
slow = self._head
|
|
492
|
+
while slow is not fast:
|
|
493
|
+
slow = slow.next
|
|
494
|
+
fast = fast.next
|
|
495
|
+
return True, slow.value
|
|
496
|
+
if return_start:
|
|
497
|
+
return False, None
|
|
498
|
+
return False
|
|
499
|
+
|
|
500
|
+
def palindrome(self) -> bool:
|
|
501
|
+
with self._lock:
|
|
502
|
+
if self._size == 0:
|
|
503
|
+
raise EmptyStructureError("palindrome on empty list")
|
|
504
|
+
values = self._to_list_unsafe()
|
|
505
|
+
return values == values[::-1]
|
|
506
|
+
|
|
507
|
+
def reorder(self, mode: str = "odd_even") -> None:
|
|
508
|
+
with self._lock:
|
|
509
|
+
if self._size <= 1:
|
|
510
|
+
return
|
|
511
|
+
self._tracer.record("reorder", mode=mode)
|
|
512
|
+
if mode == "odd_even":
|
|
513
|
+
values = self._to_list_unsafe()
|
|
514
|
+
odd_vals = [v for i, v in enumerate(values) if i % 2 == 1]
|
|
515
|
+
even_vals = [v for i, v in enumerate(values) if i % 2 == 0]
|
|
516
|
+
reordered = odd_vals + even_vals
|
|
517
|
+
node = self._head
|
|
518
|
+
for v in reordered:
|
|
519
|
+
node.value = v
|
|
520
|
+
node = node.next
|
|
521
|
+
elif mode == "zigzag":
|
|
522
|
+
nodes: list[Any] = []
|
|
523
|
+
cur = self._head
|
|
524
|
+
while cur is not None:
|
|
525
|
+
nodes.append(cur)
|
|
526
|
+
cur = cur.next
|
|
527
|
+
for i in range(len(nodes) - 1):
|
|
528
|
+
a, b = nodes[i].value, nodes[i + 1].value
|
|
529
|
+
if i % 2 == 0:
|
|
530
|
+
if a > b:
|
|
531
|
+
nodes[i].value, nodes[i + 1].value = b, a
|
|
532
|
+
else:
|
|
533
|
+
if a < b:
|
|
534
|
+
nodes[i].value, nodes[i + 1].value = b, a
|
|
535
|
+
else:
|
|
536
|
+
raise ValidationError(
|
|
537
|
+
f"Unknown reorder mode {mode!r}. Choose 'odd_even' or 'zigzag'."
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
def segregate_even_odd(self) -> None:
|
|
541
|
+
with self._lock:
|
|
542
|
+
if self._size <= 1:
|
|
543
|
+
return
|
|
544
|
+
self._tracer.record("segregate_even_odd")
|
|
545
|
+
evens: list[Any] = []
|
|
546
|
+
odds: list[Any] = []
|
|
547
|
+
node = self._head
|
|
548
|
+
for _ in range(self._size):
|
|
549
|
+
try:
|
|
550
|
+
if isinstance(node.value, int) and node.value % 2 == 0:
|
|
551
|
+
evens.append(node.value)
|
|
552
|
+
else:
|
|
553
|
+
odds.append(node.value)
|
|
554
|
+
except (TypeError, ValueError):
|
|
555
|
+
odds.append(node.value)
|
|
556
|
+
node = node.next
|
|
557
|
+
all_values = evens + odds
|
|
558
|
+
node = self._head
|
|
559
|
+
for v in all_values:
|
|
560
|
+
node.value = v
|
|
561
|
+
node = node.next
|
|
562
|
+
|
|
563
|
+
# ------------------------------------------------------------------ #
|
|
564
|
+
# Public properties #
|
|
565
|
+
# ------------------------------------------------------------------ #
|
|
566
|
+
|
|
567
|
+
@property
|
|
568
|
+
def head(self) -> Any:
|
|
569
|
+
with self._lock:
|
|
570
|
+
return self._head
|
|
571
|
+
|
|
572
|
+
@property
|
|
573
|
+
def tail(self) -> Any:
|
|
574
|
+
with self._lock:
|
|
575
|
+
return self._tail
|
|
576
|
+
|
|
577
|
+
# ------------------------------------------------------------------ #
|
|
578
|
+
# Dunder methods #
|
|
579
|
+
# ------------------------------------------------------------------ #
|
|
580
|
+
|
|
581
|
+
def __iter__(self) -> Iterator[T]:
|
|
582
|
+
with self._lock:
|
|
583
|
+
snapshot = self._to_list_unsafe()
|
|
584
|
+
return iter(snapshot)
|
|
585
|
+
|
|
586
|
+
def __len__(self) -> int:
|
|
587
|
+
return self.size()
|
|
588
|
+
|
|
589
|
+
def __bool__(self) -> bool:
|
|
590
|
+
return self.size() > 0
|
|
591
|
+
|
|
592
|
+
def __repr__(self) -> str:
|
|
593
|
+
return f"{type(self).__name__}({self.to_list()!r})"
|
|
594
|
+
|
|
595
|
+
def __contains__(self, item: object) -> bool:
|
|
596
|
+
with self._lock:
|
|
597
|
+
return self._find_node(item) is not None
|
|
598
|
+
|
|
599
|
+
def __getitem__(self, index: int) -> T:
|
|
600
|
+
return self.get(index)
|
|
601
|
+
|
|
602
|
+
def __setitem__(self, index: int, value: T) -> None:
|
|
603
|
+
self.replace(new_value=value, position=index)
|
|
604
|
+
|
|
605
|
+
def __eq__(self, other: object) -> bool:
|
|
606
|
+
if type(other) is not type(self):
|
|
607
|
+
return NotImplemented
|
|
608
|
+
return self.to_list() == other.to_list()
|