linkedset 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.
- linkedset/__init__.py +606 -0
- linkedset/py.typed +0 -0
- linkedset-0.1.0.dist-info/METADATA +199 -0
- linkedset-0.1.0.dist-info/RECORD +6 -0
- linkedset-0.1.0.dist-info/WHEEL +4 -0
- linkedset-0.1.0.dist-info/licenses/LICENSE +21 -0
linkedset/__init__.py
ADDED
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
# Copyright (c) ONNX Project Contributors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
# Modifications Copyright (c) Justin Chu
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
"""Mutable, ordered set with safe mutation properties."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Iterable, Iterator, MutableSet, Sequence
|
|
10
|
+
from typing import Generic, TypeVar, overload
|
|
11
|
+
|
|
12
|
+
__all__ = ["DoublyLinkedSet"]
|
|
13
|
+
|
|
14
|
+
_T = TypeVar("_T")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _LinkBox(Generic[_T]):
|
|
18
|
+
"""A link in a doubly linked list that has a reference to the actual object in the link.
|
|
19
|
+
|
|
20
|
+
The :class:`_LinkBox` is a container for the actual object in the list. It is used to
|
|
21
|
+
maintain the links between the elements in the linked list. The actual object is stored in the
|
|
22
|
+
:attr:`value` attribute.
|
|
23
|
+
|
|
24
|
+
By using a separate container for the actual object, we can safely remove the object from the
|
|
25
|
+
list without losing the links. This allows us to remove the object from the list during
|
|
26
|
+
iteration and place the object into a different list without breaking any chains.
|
|
27
|
+
|
|
28
|
+
This is an internal class and should only be initialized by the :class:`DoublyLinkedSet`.
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
prev: The previous box in the list.
|
|
32
|
+
next: The next box in the list.
|
|
33
|
+
erased: A flag to indicate if the box has been removed from the list.
|
|
34
|
+
owning_list: The :class:`DoublyLinkedSet` to which the box belongs.
|
|
35
|
+
value: The actual object in the list.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
__slots__ = ("next", "owning_list", "prev", "value")
|
|
39
|
+
|
|
40
|
+
def __init__(self, owner: DoublyLinkedSet[_T], value: _T | None) -> None:
|
|
41
|
+
"""Create a new link box.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
owner: The linked list to which this box belongs.
|
|
45
|
+
value: The value to be stored in the link box. When the value is None,
|
|
46
|
+
the link box is considered erased (default). The root box of the list
|
|
47
|
+
should be created with a None value.
|
|
48
|
+
"""
|
|
49
|
+
self.prev: _LinkBox[_T] = self
|
|
50
|
+
self.next: _LinkBox[_T] = self
|
|
51
|
+
self.value: _T | None = value
|
|
52
|
+
self.owning_list: DoublyLinkedSet[_T] = owner
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def erased(self) -> bool:
|
|
56
|
+
return self.value is None
|
|
57
|
+
|
|
58
|
+
def erase(self) -> None:
|
|
59
|
+
"""Remove the link from the list and detach the value from the box."""
|
|
60
|
+
if self.value is None:
|
|
61
|
+
raise ValueError("_LinkBox is already erased")
|
|
62
|
+
# Update the links
|
|
63
|
+
prev, next_ = self.prev, self.next
|
|
64
|
+
prev.next, next_.prev = next_, prev
|
|
65
|
+
# Detach the value
|
|
66
|
+
self.value = None
|
|
67
|
+
|
|
68
|
+
def __repr__(self) -> str:
|
|
69
|
+
return (
|
|
70
|
+
f"_LinkBox({self.value!r}, erased={self.erased}, "
|
|
71
|
+
f"prev={self.prev.value!r}, next={self.next.value!r})"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class DoublyLinkedSet(Sequence[_T], MutableSet[_T], Generic[_T]):
|
|
76
|
+
"""A doubly linked ordered set of nodes.
|
|
77
|
+
|
|
78
|
+
The container is both a :class:`collections.abc.Sequence` (ordered, indexable) and a
|
|
79
|
+
:class:`collections.abc.MutableSet`. It does not allow duplicate values and maintains
|
|
80
|
+
insertion order. One can treat it as a doubly linked list with list-like methods, or as
|
|
81
|
+
a mutable set supporting ``&``, ``|``, ``-``, ``^`` and their in-place variants.
|
|
82
|
+
|
|
83
|
+
Adding and removing elements from the set during iteration is safe. Moving elements
|
|
84
|
+
from one set to another is also safe.
|
|
85
|
+
|
|
86
|
+
During the iteration:
|
|
87
|
+
|
|
88
|
+
- If new elements are inserted after the current node, the iterator will
|
|
89
|
+
iterate over them as well.
|
|
90
|
+
- If new elements are inserted before the current node, they will
|
|
91
|
+
not be iterated over in this iteration.
|
|
92
|
+
- If the current node is lifted and inserted in a different location,
|
|
93
|
+
iteration will start from the "next" node at the _original_ location.
|
|
94
|
+
|
|
95
|
+
Time complexity:
|
|
96
|
+
Inserting and removing nodes from the set is O(1). Accessing nodes by index is O(n),
|
|
97
|
+
although accessing nodes at either end of the set is O(1). I.e.
|
|
98
|
+
``linked_set[0]`` and ``linked_set[-1]`` are O(1).
|
|
99
|
+
|
|
100
|
+
Membership, uniqueness and set operations are based on object **identity** (``id(value)``),
|
|
101
|
+
not equality. Two distinct objects that compare equal are treated as different elements.
|
|
102
|
+
Because it is a mutable set, instances are not hashable. Since the set is *ordered*, ``==``
|
|
103
|
+
is order-sensitive: two sets are equal only when they hold the same elements (by identity)
|
|
104
|
+
in the same order. ``None`` is not a valid value in the set.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
__slots__ = ("_length", "_root", "_value_ids_to_boxes")
|
|
108
|
+
|
|
109
|
+
def __init__(self, values: Iterable[_T] | None = None) -> None:
|
|
110
|
+
# Using the root node simplifies the mutation implementation a lot
|
|
111
|
+
# The list is circular. The root node is the only node that is not a part of the list values
|
|
112
|
+
root_ = _LinkBox(self, None)
|
|
113
|
+
self._root: _LinkBox = root_
|
|
114
|
+
self._length = 0
|
|
115
|
+
self._value_ids_to_boxes: dict[int, _LinkBox] = {}
|
|
116
|
+
if values is not None:
|
|
117
|
+
self.extend(values)
|
|
118
|
+
|
|
119
|
+
def __iter__(self) -> Iterator[_T]:
|
|
120
|
+
"""Iterate over the elements in the list.
|
|
121
|
+
|
|
122
|
+
- If new elements are inserted after the current node, the iterator will
|
|
123
|
+
iterate over them as well.
|
|
124
|
+
- If new elements are inserted before the current node, they will
|
|
125
|
+
not be iterated over in this iteration.
|
|
126
|
+
- If the current node is lifted and inserted in a different location,
|
|
127
|
+
iteration will start from the "next" node at the _original_ location.
|
|
128
|
+
"""
|
|
129
|
+
box = self._root.next
|
|
130
|
+
while box is not self._root:
|
|
131
|
+
if box.owning_list is not self:
|
|
132
|
+
raise RuntimeError(f"Element {box!r} is not in the list")
|
|
133
|
+
if not box.erased:
|
|
134
|
+
assert box.value is not None
|
|
135
|
+
yield box.value
|
|
136
|
+
box = box.next
|
|
137
|
+
|
|
138
|
+
def __reversed__(self) -> Iterator[_T]:
|
|
139
|
+
"""Iterate over the elements in the list in reverse order."""
|
|
140
|
+
box = self._root.prev
|
|
141
|
+
while box is not self._root:
|
|
142
|
+
if not box.erased:
|
|
143
|
+
assert box.value is not None
|
|
144
|
+
yield box.value
|
|
145
|
+
box = box.prev
|
|
146
|
+
|
|
147
|
+
def __len__(self) -> int:
|
|
148
|
+
assert self._length == len(self._value_ids_to_boxes), (
|
|
149
|
+
"Bug in the implementation: length mismatch"
|
|
150
|
+
)
|
|
151
|
+
return self._length
|
|
152
|
+
|
|
153
|
+
def __contains__(self, value: object) -> bool:
|
|
154
|
+
"""Return whether ``value`` is in the set, using object identity."""
|
|
155
|
+
return id(value) in self._value_ids_to_boxes
|
|
156
|
+
|
|
157
|
+
def __eq__(self, other: object) -> bool:
|
|
158
|
+
"""Return whether ``other`` is an equal, equally-ordered set.
|
|
159
|
+
|
|
160
|
+
Because :class:`DoublyLinkedSet` is *ordered*, equality is order-sensitive
|
|
161
|
+
(unlike a plain :class:`set`). Two sets are equal only when they contain the same
|
|
162
|
+
elements, by object identity, in the same order. Only another
|
|
163
|
+
:class:`DoublyLinkedSet` can compare equal.
|
|
164
|
+
"""
|
|
165
|
+
if self is other:
|
|
166
|
+
return True
|
|
167
|
+
if not isinstance(other, DoublyLinkedSet):
|
|
168
|
+
return NotImplemented
|
|
169
|
+
if self._length != other._length:
|
|
170
|
+
return False
|
|
171
|
+
return all(a is b for a, b in zip(self, other))
|
|
172
|
+
|
|
173
|
+
__hash__ = None # type: ignore[assignment] # ordered, mutable -> unhashable
|
|
174
|
+
|
|
175
|
+
def count(self, value: object) -> int:
|
|
176
|
+
"""Return the number of occurrences of ``value`` (0 or 1), using object identity."""
|
|
177
|
+
return 1 if id(value) in self._value_ids_to_boxes else 0
|
|
178
|
+
|
|
179
|
+
def index(self, value: object, start: int = 0, stop: int | None = None) -> int:
|
|
180
|
+
"""Return the index of ``value`` in the set, using object identity.
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
ValueError: If ``value`` is not present in ``self[start:stop]``.
|
|
184
|
+
"""
|
|
185
|
+
length = self._length
|
|
186
|
+
if start < 0:
|
|
187
|
+
start = max(length + start, 0)
|
|
188
|
+
if stop is None:
|
|
189
|
+
stop = length
|
|
190
|
+
elif stop < 0:
|
|
191
|
+
stop += length
|
|
192
|
+
for i, item in enumerate(self):
|
|
193
|
+
if i >= stop:
|
|
194
|
+
break
|
|
195
|
+
if i >= start and item is value:
|
|
196
|
+
return i
|
|
197
|
+
raise ValueError(f"{value!r} is not in the set")
|
|
198
|
+
|
|
199
|
+
@overload
|
|
200
|
+
def __getitem__(self, index: int) -> _T: ...
|
|
201
|
+
@overload
|
|
202
|
+
def __getitem__(self, index: slice) -> Sequence[_T]: ...
|
|
203
|
+
|
|
204
|
+
def __getitem__(self, index):
|
|
205
|
+
"""Get the node at the given index.
|
|
206
|
+
|
|
207
|
+
Complexity is O(n).
|
|
208
|
+
"""
|
|
209
|
+
if isinstance(index, slice):
|
|
210
|
+
return tuple(self)[index]
|
|
211
|
+
if index >= self._length or index < -self._length:
|
|
212
|
+
raise IndexError(
|
|
213
|
+
f"Index out of range: {index} not in range [-{self._length}, {self._length})"
|
|
214
|
+
)
|
|
215
|
+
if index < 0:
|
|
216
|
+
# Look up from the end of the list
|
|
217
|
+
iterator = reversed(self)
|
|
218
|
+
item = next(iterator)
|
|
219
|
+
for _ in range(-index - 1):
|
|
220
|
+
item = next(iterator)
|
|
221
|
+
else:
|
|
222
|
+
iterator = iter(self) # type: ignore[assignment]
|
|
223
|
+
item = next(iterator)
|
|
224
|
+
for _ in range(index):
|
|
225
|
+
item = next(iterator)
|
|
226
|
+
return item
|
|
227
|
+
|
|
228
|
+
def _insert_one_after(
|
|
229
|
+
self,
|
|
230
|
+
box: _LinkBox[_T],
|
|
231
|
+
new_value: _T,
|
|
232
|
+
) -> _LinkBox[_T]:
|
|
233
|
+
"""Insert a new value after the given box.
|
|
234
|
+
|
|
235
|
+
All insertion methods should call this method to ensure that the list is updated correctly.
|
|
236
|
+
|
|
237
|
+
Example::
|
|
238
|
+
Before: A <-> B <-> C
|
|
239
|
+
^v0 ^v1 ^v2
|
|
240
|
+
Call: _insert_one_after(B, v3)
|
|
241
|
+
After: A <-> B <-> new_box <-> C
|
|
242
|
+
^v0 ^v1 ^v3 ^v2
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
box: The box which the new value is to be inserted.
|
|
246
|
+
new_value: The new value to be inserted.
|
|
247
|
+
"""
|
|
248
|
+
if new_value is None:
|
|
249
|
+
raise TypeError(f"{self.__class__.__name__} does not support None values")
|
|
250
|
+
if box.value is new_value:
|
|
251
|
+
# Do nothing if the new value is the same as the old value
|
|
252
|
+
return box
|
|
253
|
+
if box.owning_list is not self:
|
|
254
|
+
raise ValueError(f"Value {box.value!r} is not in the list")
|
|
255
|
+
|
|
256
|
+
if (new_value_id := id(new_value)) in self._value_ids_to_boxes:
|
|
257
|
+
# If the value is already in the list, remove it first
|
|
258
|
+
self.remove(new_value)
|
|
259
|
+
|
|
260
|
+
# Create a new _LinkBox for the new value
|
|
261
|
+
new_box = _LinkBox(self, new_value)
|
|
262
|
+
# original_box <=> original_next
|
|
263
|
+
# becomes
|
|
264
|
+
# original_box <=> new_box <=> original_next
|
|
265
|
+
original_next = box.next
|
|
266
|
+
box.next = new_box
|
|
267
|
+
new_box.prev = box
|
|
268
|
+
new_box.next = original_next
|
|
269
|
+
original_next.prev = new_box
|
|
270
|
+
|
|
271
|
+
# Be sure to update the length and mapping
|
|
272
|
+
self._length += 1
|
|
273
|
+
self._value_ids_to_boxes[new_value_id] = new_box
|
|
274
|
+
|
|
275
|
+
return new_box
|
|
276
|
+
|
|
277
|
+
def _insert_many_after(
|
|
278
|
+
self,
|
|
279
|
+
box: _LinkBox[_T],
|
|
280
|
+
new_values: Iterable[_T],
|
|
281
|
+
):
|
|
282
|
+
"""Insert multiple new values after the given box."""
|
|
283
|
+
insertion_point = box
|
|
284
|
+
for new_value in new_values:
|
|
285
|
+
insertion_point = self._insert_one_after(insertion_point, new_value)
|
|
286
|
+
|
|
287
|
+
def remove(self, value: _T) -> None:
|
|
288
|
+
"""Remove a node from the list."""
|
|
289
|
+
if (value_id := id(value)) not in self._value_ids_to_boxes:
|
|
290
|
+
raise ValueError(f"Value {value!r} is not in the list")
|
|
291
|
+
box = self._value_ids_to_boxes[value_id]
|
|
292
|
+
# Remove the link box and detach the value from the box
|
|
293
|
+
box.erase()
|
|
294
|
+
|
|
295
|
+
# Be sure to update the length and mapping
|
|
296
|
+
self._length -= 1
|
|
297
|
+
del self._value_ids_to_boxes[value_id]
|
|
298
|
+
|
|
299
|
+
def add(self, value: _T) -> None:
|
|
300
|
+
"""Add ``value`` to the end of the set if it is not already present.
|
|
301
|
+
|
|
302
|
+
Unlike :meth:`append`, this is idempotent: if ``value`` (by identity) is already
|
|
303
|
+
in the set, its position is left unchanged. This implements
|
|
304
|
+
:meth:`collections.abc.MutableSet.add`.
|
|
305
|
+
"""
|
|
306
|
+
if id(value) not in self._value_ids_to_boxes:
|
|
307
|
+
self.append(value)
|
|
308
|
+
|
|
309
|
+
def discard(self, value: _T) -> None:
|
|
310
|
+
"""Remove ``value`` from the set if it is present, without raising otherwise.
|
|
311
|
+
|
|
312
|
+
This implements :meth:`collections.abc.MutableSet.discard`.
|
|
313
|
+
"""
|
|
314
|
+
if id(value) in self._value_ids_to_boxes:
|
|
315
|
+
self.remove(value)
|
|
316
|
+
|
|
317
|
+
def append(self, value: _T) -> None:
|
|
318
|
+
"""Append a node to the list."""
|
|
319
|
+
_ = self._insert_one_after(self._root.prev, value)
|
|
320
|
+
|
|
321
|
+
def extend(
|
|
322
|
+
self,
|
|
323
|
+
values: Iterable[_T],
|
|
324
|
+
) -> None:
|
|
325
|
+
for value in values:
|
|
326
|
+
self.append(value)
|
|
327
|
+
|
|
328
|
+
def appendleft(self, value: _T) -> None:
|
|
329
|
+
"""Add ``value`` to the front of the set (deque-style).
|
|
330
|
+
|
|
331
|
+
If ``value`` is already present (by identity), it is moved to the front.
|
|
332
|
+
"""
|
|
333
|
+
_ = self._insert_one_after(self._root, value)
|
|
334
|
+
|
|
335
|
+
def extendleft(self, values: Iterable[_T]) -> None:
|
|
336
|
+
"""Prepend each value in ``values`` to the front of the set (deque-style).
|
|
337
|
+
|
|
338
|
+
As with :meth:`collections.deque.extendleft`, the prepended items end up in reverse
|
|
339
|
+
order relative to ``values``.
|
|
340
|
+
"""
|
|
341
|
+
for value in values:
|
|
342
|
+
self.appendleft(value)
|
|
343
|
+
|
|
344
|
+
def _box_at(self, index: int) -> _LinkBox[_T]:
|
|
345
|
+
"""Return the live link box at ``index`` in ``[0, len)``, walking from the nearer end."""
|
|
346
|
+
if index <= self._length - 1 - index:
|
|
347
|
+
box = self._root.next
|
|
348
|
+
for _ in range(index):
|
|
349
|
+
box = box.next
|
|
350
|
+
else:
|
|
351
|
+
box = self._root.prev
|
|
352
|
+
for _ in range(self._length - 1 - index):
|
|
353
|
+
box = box.prev
|
|
354
|
+
return box
|
|
355
|
+
|
|
356
|
+
def insert(self, index: int, value: _T) -> None:
|
|
357
|
+
"""Insert ``value`` before position ``index`` (list/deque-style).
|
|
358
|
+
|
|
359
|
+
``index`` is clamped like :meth:`list.insert`. If ``value`` is already present it is
|
|
360
|
+
removed first, so ``index`` refers to a position in the resulting sequence.
|
|
361
|
+
"""
|
|
362
|
+
if value is None:
|
|
363
|
+
raise TypeError(f"{self.__class__.__name__} does not support None values")
|
|
364
|
+
if id(value) in self._value_ids_to_boxes:
|
|
365
|
+
self.remove(value)
|
|
366
|
+
length = self._length
|
|
367
|
+
if index < 0:
|
|
368
|
+
index = max(length + index, 0)
|
|
369
|
+
if index >= length:
|
|
370
|
+
self.append(value)
|
|
371
|
+
return
|
|
372
|
+
target = self._box_at(index)
|
|
373
|
+
self._insert_one_after(target.prev, value)
|
|
374
|
+
|
|
375
|
+
def pop(self, index: int = -1) -> _T:
|
|
376
|
+
"""Remove and return the value at ``index`` (default: the last one, list-style).
|
|
377
|
+
|
|
378
|
+
Raises:
|
|
379
|
+
IndexError: If the set is empty or ``index`` is out of range.
|
|
380
|
+
"""
|
|
381
|
+
if self._length == 0:
|
|
382
|
+
raise IndexError("pop from empty DoublyLinkedSet")
|
|
383
|
+
value = self[index]
|
|
384
|
+
self.remove(value)
|
|
385
|
+
return value
|
|
386
|
+
|
|
387
|
+
def popleft(self) -> _T:
|
|
388
|
+
"""Remove and return the first value (deque-style).
|
|
389
|
+
|
|
390
|
+
Raises:
|
|
391
|
+
IndexError: If the set is empty.
|
|
392
|
+
"""
|
|
393
|
+
if self._length == 0:
|
|
394
|
+
raise IndexError("popleft from empty DoublyLinkedSet")
|
|
395
|
+
value = self[0]
|
|
396
|
+
self.remove(value)
|
|
397
|
+
return value
|
|
398
|
+
|
|
399
|
+
def clear(self) -> None:
|
|
400
|
+
"""Remove all elements from the set.
|
|
401
|
+
|
|
402
|
+
Safe to call during iteration: existing nodes are marked erased (so a live iterator
|
|
403
|
+
stops yielding them) before the set is reset.
|
|
404
|
+
"""
|
|
405
|
+
box = self._root.next
|
|
406
|
+
while box is not self._root:
|
|
407
|
+
# Detach the value but keep next/prev intact so a live iterator can still advance.
|
|
408
|
+
nxt = box.next
|
|
409
|
+
box.value = None
|
|
410
|
+
box = nxt
|
|
411
|
+
self._root.prev = self._root
|
|
412
|
+
self._root.next = self._root
|
|
413
|
+
self._length = 0
|
|
414
|
+
self._value_ids_to_boxes.clear()
|
|
415
|
+
|
|
416
|
+
def reverse(self) -> None:
|
|
417
|
+
"""Reverse the elements of the set in place.
|
|
418
|
+
|
|
419
|
+
Note:
|
|
420
|
+
Unlike per-element mutation, this is a global reorder and is **not** safe to call
|
|
421
|
+
while iterating over the set: an in-progress iterator may then skip or repeat
|
|
422
|
+
elements.
|
|
423
|
+
"""
|
|
424
|
+
node = self._root.next
|
|
425
|
+
while node is not self._root:
|
|
426
|
+
nxt = node.next
|
|
427
|
+
node.next, node.prev = node.prev, node.next
|
|
428
|
+
node = nxt
|
|
429
|
+
self._root.next, self._root.prev = self._root.prev, self._root.next
|
|
430
|
+
|
|
431
|
+
def rotate(self, n: int = 1) -> None:
|
|
432
|
+
"""Rotate the set ``n`` steps to the right (deque-style).
|
|
433
|
+
|
|
434
|
+
Negative ``n`` rotates to the left. ``rotate(1)`` moves the last element to the front.
|
|
435
|
+
|
|
436
|
+
Note:
|
|
437
|
+
Like :meth:`reverse`, this is a global reorder and is **not** safe to call while
|
|
438
|
+
iterating over the set.
|
|
439
|
+
"""
|
|
440
|
+
length = self._length
|
|
441
|
+
if length <= 1:
|
|
442
|
+
return
|
|
443
|
+
n %= length
|
|
444
|
+
if n == 0:
|
|
445
|
+
return
|
|
446
|
+
# The new first element is the n-th element counted from the end.
|
|
447
|
+
new_head = self._root.prev
|
|
448
|
+
for _ in range(n - 1):
|
|
449
|
+
new_head = new_head.prev
|
|
450
|
+
new_tail = new_head.prev
|
|
451
|
+
old_head = self._root.next
|
|
452
|
+
old_tail = self._root.prev
|
|
453
|
+
# Close the value ring, removing the root sentinel from between old_tail and old_head.
|
|
454
|
+
old_tail.next = old_head
|
|
455
|
+
old_head.prev = old_tail
|
|
456
|
+
# Splice the root sentinel back in between new_tail and new_head.
|
|
457
|
+
new_tail.next = self._root
|
|
458
|
+
self._root.prev = new_tail
|
|
459
|
+
self._root.next = new_head
|
|
460
|
+
new_head.prev = self._root
|
|
461
|
+
|
|
462
|
+
def copy(self) -> DoublyLinkedSet[_T]:
|
|
463
|
+
"""Return a shallow copy of the set, preserving order."""
|
|
464
|
+
return DoublyLinkedSet(self)
|
|
465
|
+
|
|
466
|
+
# -- Set algebra -------------------------------------------------------------------------
|
|
467
|
+
# These override the collections.abc.Set mixins so that they use identity (``id``)
|
|
468
|
+
# semantics consistently and preserve this set's (left-operand) order, even when the other
|
|
469
|
+
# operand is an equality-based container such as a built-in ``set``.
|
|
470
|
+
|
|
471
|
+
@staticmethod
|
|
472
|
+
def _ids_of(iterable: Iterable[object]) -> set[int]:
|
|
473
|
+
return {id(value) for value in iterable}
|
|
474
|
+
|
|
475
|
+
def __and__(self, other: Iterable[_T]) -> DoublyLinkedSet[_T]:
|
|
476
|
+
if not isinstance(other, Iterable):
|
|
477
|
+
return NotImplemented
|
|
478
|
+
other_ids = self._ids_of(other)
|
|
479
|
+
result: DoublyLinkedSet[_T] = DoublyLinkedSet()
|
|
480
|
+
for value in self:
|
|
481
|
+
if id(value) in other_ids:
|
|
482
|
+
result.append(value)
|
|
483
|
+
return result
|
|
484
|
+
|
|
485
|
+
def __rand__(self, other: Iterable[_T]) -> DoublyLinkedSet[_T]:
|
|
486
|
+
# Reflected: ``other`` is the left operand, so honour its order.
|
|
487
|
+
if not isinstance(other, Iterable):
|
|
488
|
+
return NotImplemented
|
|
489
|
+
result: DoublyLinkedSet[_T] = DoublyLinkedSet()
|
|
490
|
+
for value in other:
|
|
491
|
+
if id(value) in self._value_ids_to_boxes:
|
|
492
|
+
result.append(value)
|
|
493
|
+
return result
|
|
494
|
+
|
|
495
|
+
def __or__(self, other: Iterable[_T]) -> DoublyLinkedSet[_T]:
|
|
496
|
+
if not isinstance(other, Iterable):
|
|
497
|
+
return NotImplemented
|
|
498
|
+
result = self.copy()
|
|
499
|
+
for value in other:
|
|
500
|
+
result.add(value)
|
|
501
|
+
return result
|
|
502
|
+
|
|
503
|
+
def __ror__(self, other: Iterable[_T]) -> DoublyLinkedSet[_T]:
|
|
504
|
+
if not isinstance(other, Iterable):
|
|
505
|
+
return NotImplemented
|
|
506
|
+
result: DoublyLinkedSet[_T] = DoublyLinkedSet(other)
|
|
507
|
+
for value in self:
|
|
508
|
+
result.add(value)
|
|
509
|
+
return result
|
|
510
|
+
|
|
511
|
+
def __sub__(self, other: Iterable[_T]) -> DoublyLinkedSet[_T]:
|
|
512
|
+
if not isinstance(other, Iterable):
|
|
513
|
+
return NotImplemented
|
|
514
|
+
other_ids = self._ids_of(other)
|
|
515
|
+
result: DoublyLinkedSet[_T] = DoublyLinkedSet()
|
|
516
|
+
for value in self:
|
|
517
|
+
if id(value) not in other_ids:
|
|
518
|
+
result.append(value)
|
|
519
|
+
return result
|
|
520
|
+
|
|
521
|
+
def __rsub__(self, other: Iterable[_T]) -> DoublyLinkedSet[_T]:
|
|
522
|
+
if not isinstance(other, Iterable):
|
|
523
|
+
return NotImplemented
|
|
524
|
+
result: DoublyLinkedSet[_T] = DoublyLinkedSet()
|
|
525
|
+
for value in other:
|
|
526
|
+
if id(value) not in self._value_ids_to_boxes:
|
|
527
|
+
result.append(value)
|
|
528
|
+
return result
|
|
529
|
+
|
|
530
|
+
def __xor__(self, other: Iterable[_T]) -> DoublyLinkedSet[_T]:
|
|
531
|
+
if not isinstance(other, Iterable):
|
|
532
|
+
return NotImplemented
|
|
533
|
+
other_values = list(other)
|
|
534
|
+
other_ids = self._ids_of(other_values)
|
|
535
|
+
result: DoublyLinkedSet[_T] = DoublyLinkedSet()
|
|
536
|
+
for value in self:
|
|
537
|
+
if id(value) not in other_ids:
|
|
538
|
+
result.append(value)
|
|
539
|
+
for value in other_values:
|
|
540
|
+
if id(value) not in self._value_ids_to_boxes:
|
|
541
|
+
result.add(value)
|
|
542
|
+
return result
|
|
543
|
+
|
|
544
|
+
def __rxor__(self, other: Iterable[_T]) -> DoublyLinkedSet[_T]:
|
|
545
|
+
# Reflected: ``other`` is the left operand, so emit its unique values first, in order.
|
|
546
|
+
if not isinstance(other, Iterable):
|
|
547
|
+
return NotImplemented
|
|
548
|
+
other_values = list(other)
|
|
549
|
+
other_ids = self._ids_of(other_values)
|
|
550
|
+
result: DoublyLinkedSet[_T] = DoublyLinkedSet()
|
|
551
|
+
for value in other_values:
|
|
552
|
+
if id(value) not in self._value_ids_to_boxes:
|
|
553
|
+
result.append(value)
|
|
554
|
+
for value in self:
|
|
555
|
+
if id(value) not in other_ids:
|
|
556
|
+
result.add(value)
|
|
557
|
+
return result
|
|
558
|
+
|
|
559
|
+
def isdisjoint(self, other: Iterable[_T]) -> bool:
|
|
560
|
+
"""Return ``True`` if the set has no elements (by identity) in common with ``other``."""
|
|
561
|
+
return not any(id(value) in self._value_ids_to_boxes for value in other)
|
|
562
|
+
|
|
563
|
+
def _unsupported_ordering(self, _other: object) -> bool:
|
|
564
|
+
raise TypeError(
|
|
565
|
+
f"{type(self).__name__} does not support ordering/subset comparisons "
|
|
566
|
+
"(<, <=, >, >=) because '==' is order-sensitive; "
|
|
567
|
+
"use set algebra (&, |, -, ^) or isdisjoint() instead"
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
# Subset/superset ordering would clash with the order-sensitive ``==``, so it is disabled.
|
|
571
|
+
__lt__ = __le__ = __gt__ = __ge__ = _unsupported_ordering
|
|
572
|
+
|
|
573
|
+
def insert_after(
|
|
574
|
+
self,
|
|
575
|
+
value: _T,
|
|
576
|
+
new_values: Iterable[_T],
|
|
577
|
+
) -> None:
|
|
578
|
+
"""Insert new nodes after the given node.
|
|
579
|
+
|
|
580
|
+
Args:
|
|
581
|
+
value: The value after which the new values are to be inserted.
|
|
582
|
+
new_values: The new values to be inserted.
|
|
583
|
+
"""
|
|
584
|
+
if (value_id := id(value)) not in self._value_ids_to_boxes:
|
|
585
|
+
raise ValueError(f"Value {value!r} is not in the list")
|
|
586
|
+
insertion_point = self._value_ids_to_boxes[value_id]
|
|
587
|
+
return self._insert_many_after(insertion_point, new_values)
|
|
588
|
+
|
|
589
|
+
def insert_before(
|
|
590
|
+
self,
|
|
591
|
+
value: _T,
|
|
592
|
+
new_values: Iterable[_T],
|
|
593
|
+
) -> None:
|
|
594
|
+
"""Insert new nodes before the given node.
|
|
595
|
+
|
|
596
|
+
Args:
|
|
597
|
+
value: The value before which the new values are to be inserted.
|
|
598
|
+
new_values: The new values to be inserted.
|
|
599
|
+
"""
|
|
600
|
+
if (value_id := id(value)) not in self._value_ids_to_boxes:
|
|
601
|
+
raise ValueError(f"Value {value!r} is not in the list")
|
|
602
|
+
insertion_point = self._value_ids_to_boxes[value_id].prev
|
|
603
|
+
return self._insert_many_after(insertion_point, new_values)
|
|
604
|
+
|
|
605
|
+
def __repr__(self) -> str:
|
|
606
|
+
return f"DoublyLinkedSet({list(self)})"
|
linkedset/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: linkedset
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Ordered set robust against mutation during iteration in Python
|
|
5
|
+
Project-URL: Homepage, https://github.com/justinchuby/linkedset
|
|
6
|
+
Project-URL: Documentation, https://justinchuby.github.io/linkedset/
|
|
7
|
+
Project-URL: Repository, https://github.com/justinchuby/linkedset
|
|
8
|
+
Project-URL: Issues, https://github.com/justinchuby/linkedset/issues
|
|
9
|
+
Author: Justin Chu
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: data-structure,iteration,linked-list,ordered-set
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
25
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
26
|
+
Provides-Extra: docs
|
|
27
|
+
Requires-Dist: furo; extra == 'docs'
|
|
28
|
+
Requires-Dist: myst-parser; extra == 'docs'
|
|
29
|
+
Requires-Dist: sphinx>=7; extra == 'docs'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# linkedset
|
|
33
|
+
|
|
34
|
+
An ordered set that is robust against mutation during iteration, implemented in pure Python.
|
|
35
|
+
|
|
36
|
+
`DoublyLinkedSet` behaves like an ordered set backed by a doubly linked list. Inserting
|
|
37
|
+
and removing elements is `O(1)`, and — unlike Python's built-in `list` or `set` — you can
|
|
38
|
+
safely add, remove, or move elements *while iterating over the container*.
|
|
39
|
+
|
|
40
|
+
It implements both `collections.abc.Sequence` (ordered, indexable) and
|
|
41
|
+
`collections.abc.MutableSet` (set algebra and in-place updates).
|
|
42
|
+
|
|
43
|
+
📖 **Documentation**: <https://justinchuby.github.io/linkedset/>
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install linkedset
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Or install from source:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
git clone https://github.com/justinchuby/linkedset
|
|
55
|
+
cd linkedset
|
|
56
|
+
pip install -e ".[dev]"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from linkedset import DoublyLinkedSet
|
|
63
|
+
|
|
64
|
+
s = DoublyLinkedSet(["a", "b", "c"])
|
|
65
|
+
|
|
66
|
+
s.append("d") # -> a, b, c, d
|
|
67
|
+
s.insert_after("a", ["x"]) # -> a, x, b, c, d
|
|
68
|
+
s.insert_before("b", ["y"]) # -> a, x, y, b, c, d
|
|
69
|
+
s.remove("c") # -> a, x, y, b, d
|
|
70
|
+
|
|
71
|
+
print(s[0]) # "a" (O(1))
|
|
72
|
+
print(s[-1]) # "d" (O(1))
|
|
73
|
+
print(list(s)) # ['a', 'x', 'y', 'b', 'd']
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Safe mutation during iteration
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
s = DoublyLinkedSet(["a", "b", "c"])
|
|
80
|
+
for x in s:
|
|
81
|
+
if x == "a":
|
|
82
|
+
s.insert_after("a", ["d"]) # inserted after current -> iterated
|
|
83
|
+
s.remove("b") # removed before reached -> skipped
|
|
84
|
+
# Iterated: a, d, c
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Iteration rules:
|
|
88
|
+
|
|
89
|
+
- Elements inserted **after** the current node **are** iterated over.
|
|
90
|
+
- Elements inserted **before** the current node are **not** iterated over in the current pass.
|
|
91
|
+
- If the current node is moved to a different location, iteration continues from the node
|
|
92
|
+
that followed it at its *original* location.
|
|
93
|
+
|
|
94
|
+
Per-element mutation (`add`, `remove`, `discard`, `append`, `insert`, `clear`, …) is safe
|
|
95
|
+
during iteration. The **global reorders** `reverse()` and `rotate()` are *not* — calling them
|
|
96
|
+
mid-iteration may cause an in-progress iterator to skip or repeat elements.
|
|
97
|
+
|
|
98
|
+
### Set operations
|
|
99
|
+
|
|
100
|
+
Because it is a `MutableSet`, the usual set algebra works and returns a new
|
|
101
|
+
`DoublyLinkedSet` (order preserved):
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
a = DoublyLinkedSet(["a", "b", "c"])
|
|
105
|
+
b = DoublyLinkedSet(["c", "d"])
|
|
106
|
+
|
|
107
|
+
a | b # union -> a, b, c, d
|
|
108
|
+
a & b # intersection -> c
|
|
109
|
+
a - b # difference -> a, b
|
|
110
|
+
a ^ b # symmetric -> a, b, d
|
|
111
|
+
|
|
112
|
+
a.add("x") # idempotent add (no-op if already present)
|
|
113
|
+
a.discard("z") # remove if present, never raises
|
|
114
|
+
a.pop() # remove and return the last element (list-style; pass an index too)
|
|
115
|
+
a |= b # in-place update
|
|
116
|
+
|
|
117
|
+
# `==` is order-sensitive, because the set is ordered:
|
|
118
|
+
DoublyLinkedSet(["a", "b"]) == DoublyLinkedSet(["a", "b"]) # True
|
|
119
|
+
DoublyLinkedSet(["a", "b"]) == DoublyLinkedSet(["b", "a"]) # False
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Deque- and list-style methods
|
|
123
|
+
|
|
124
|
+
Because it is ordered, it also offers the familiar `deque`/`list` mutators (all keeping the
|
|
125
|
+
set's uniqueness and identity semantics):
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
s = DoublyLinkedSet(["a", "b", "c"])
|
|
129
|
+
|
|
130
|
+
s.appendleft("z") # -> z, a, b, c
|
|
131
|
+
s.extendleft(["x", "y"])# -> y, x, z, a, b, c (prepended, reversed like deque)
|
|
132
|
+
s.popleft() # removes and returns "y"
|
|
133
|
+
s.pop() # removes and returns the last element ("c")
|
|
134
|
+
s.pop(1) # removes and returns the element at index 1
|
|
135
|
+
s.insert(1, "q") # insert before index 1 (index clamped like list.insert)
|
|
136
|
+
s.rotate(1) # rotate right; negative rotates left
|
|
137
|
+
s.reverse() # reverse in place
|
|
138
|
+
s2 = s.copy() # shallow copy, order preserved
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Semantics
|
|
142
|
+
|
|
143
|
+
- Membership and set operations are based on object **identity** (`id(value)`), not equality.
|
|
144
|
+
Two distinct objects that compare equal are treated as different elements.
|
|
145
|
+
- `==` is **order-sensitive** (it is an *ordered* set): equal only when the same elements, by
|
|
146
|
+
identity, appear in the same order. Instances are **not hashable** (mutable set).
|
|
147
|
+
Ordering/subset comparisons (`<`, `<=`, `>`, `>=`) are **not supported** (they raise
|
|
148
|
+
`TypeError`), since a subset relation would be ambiguous next to order-sensitive equality;
|
|
149
|
+
use the set algebra (`&`, `|`, `-`, `^`) or `isdisjoint()` instead.
|
|
150
|
+
- `None` is not a valid value.
|
|
151
|
+
- Accessing by index is `O(n)`, except the ends (`s[0]`, `s[-1]`) which are `O(1)`.
|
|
152
|
+
|
|
153
|
+
## Complexity
|
|
154
|
+
|
|
155
|
+
`DoublyLinkedSet` is a doubly linked list paired with a `dict` mapping each element's
|
|
156
|
+
`id()` to its list node. That combination gives set-like `O(1)` membership and endpoint
|
|
157
|
+
mutation, while preserving order and safe mutation during iteration.
|
|
158
|
+
|
|
159
|
+
Let `n` be the size of the set (and `m` the size of the other operand for binary set
|
|
160
|
+
operations).
|
|
161
|
+
|
|
162
|
+
| Operation | Complexity | Notes |
|
|
163
|
+
| --- | --- | --- |
|
|
164
|
+
| `x in s`, `s.count(x)` | `O(1)` | `dict` lookup by `id(x)` |
|
|
165
|
+
| `len(s)` | `O(1)` | length is tracked, not counted |
|
|
166
|
+
| `s.append(x)`, `s.add(x)`, `s.appendleft(x)` | `O(1)` | insert at a known end |
|
|
167
|
+
| `s.remove(x)`, `s.discard(x)` | `O(1)` | unlink the node, no shifting |
|
|
168
|
+
| `s.extend(xs)`, `s.extendleft(xs)` | `O(k)` | `k = len(xs)`; `O(1)` per element |
|
|
169
|
+
| `s.insert_after(v, xs)`, `s.insert_before(v, xs)` | `O(k)` | `k = len(xs)`; `O(1)` per element |
|
|
170
|
+
| `s.pop()`, `s.popleft()` | `O(1)` | remove from an end |
|
|
171
|
+
| `s.pop(i)`, `s.insert(i, x)` | `O(\|i\|)` | walks to the index from the nearer end |
|
|
172
|
+
| `s[0]`, `s[-1]` | `O(1)` | endpoints are reachable directly |
|
|
173
|
+
| `s[i]` | `O(\|i\|)` | walks from the nearer end |
|
|
174
|
+
| `s.index(x)` | `O(n)` | linear scan for position |
|
|
175
|
+
| `s.rotate(n)` | `O(n mod len(s))` | relink only, no node churn |
|
|
176
|
+
| `s.reverse()`, `s.copy()`, `s.clear()` | `O(n)` | |
|
|
177
|
+
| iteration, `reversed(s)`, `s == other` | `O(n)` | |
|
|
178
|
+
| `s[a:b]` (slice) | `O(n)` | materialises a tuple |
|
|
179
|
+
| `s \| t`, `s & t`, `s - t`, `s ^ t` | `O(n + m)` | each membership test is `O(1)` |
|
|
180
|
+
| `s.isdisjoint(t)` | `O(n)` | one pass with `O(1)` lookups |
|
|
181
|
+
|
|
182
|
+
Space is `O(n)`: every element is wrapped in a small link node and referenced once from the
|
|
183
|
+
`id`-keyed index.
|
|
184
|
+
|
|
185
|
+
Mutating during iteration stays `O(1)` per operation. Removed nodes are unlinked from their
|
|
186
|
+
neighbours immediately, so a traversal never pays to skip over dead nodes — the only cost is
|
|
187
|
+
following the `next` pointer you already hold.
|
|
188
|
+
|
|
189
|
+
## Development
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
pip install -e ".[dev]"
|
|
193
|
+
python -m pytest # run tests
|
|
194
|
+
python -m ruff check # lint
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
MIT. Portions derived from the ONNX Project Contributors (Apache-2.0); see `linkedset/__init__.py`.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
linkedset/__init__.py,sha256=56ZBs9dwjCFopipD0KZyRGZ5Hx4qGn5J3FLUjOiF_5o,23253
|
|
2
|
+
linkedset/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
linkedset-0.1.0.dist-info/METADATA,sha256=X8Wk5mps2SCCDzSF7OZTHJtSdukv_4hQH5Xa1XB5M9g,7672
|
|
4
|
+
linkedset-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
5
|
+
linkedset-0.1.0.dist-info/licenses/LICENSE,sha256=SjqpNtkAu8i5L-1mHSpuWDPmthnFqwIMpsicZMARlhM,1067
|
|
6
|
+
linkedset-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Justin Chu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|