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,258 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pkstruct.linear.queues.circular_queue
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
A fixed-capacity FIFO queue backed by a ring buffer (circular array).
|
|
5
|
+
|
|
6
|
+
``CircularQueue`` uses a pre-allocated Python ``list`` with head and tail
|
|
7
|
+
pointers that wrap around. Enqueue and dequeue are O(1). Once the buffer
|
|
8
|
+
is full, further enqueue operations raise ``QueueFullError``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import Iterator
|
|
14
|
+
from typing import TypeVar
|
|
15
|
+
|
|
16
|
+
from pkstruct.linear.exceptions import EmptyStructureError
|
|
17
|
+
from pkstruct.linear.queues.queue import Queue
|
|
18
|
+
from pkstruct.shared.threading import StructureLock
|
|
19
|
+
|
|
20
|
+
T = TypeVar("T")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class QueueFullError(IndexError):
|
|
24
|
+
"""Raised when attempting to enqueue into a full ``CircularQueue``."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, capacity: int) -> None:
|
|
27
|
+
self.capacity: int = capacity
|
|
28
|
+
super().__init__(f"Queue is at capacity ({capacity}).")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CircularQueue(Queue[T]):
|
|
32
|
+
"""
|
|
33
|
+
A thread-safe, fixed-capacity FIFO queue using a ring buffer.
|
|
34
|
+
|
|
35
|
+
The underlying ``list`` is allocated once at creation. Head and tail
|
|
36
|
+
indices advance modulo capacity, avoiding the O(n) cost of shifting
|
|
37
|
+
elements.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
capacity:
|
|
42
|
+
Maximum number of elements the queue can hold.
|
|
43
|
+
items:
|
|
44
|
+
Optional iterable of initial values (front-to-rear order).
|
|
45
|
+
``len(items)`` must not exceed *capacity*.
|
|
46
|
+
|
|
47
|
+
Examples
|
|
48
|
+
--------
|
|
49
|
+
>>> q = CircularQueue(3, [1, 2])
|
|
50
|
+
>>> q.enqueue(3)
|
|
51
|
+
>>> q.dequeue()
|
|
52
|
+
1
|
|
53
|
+
>>> q.front()
|
|
54
|
+
2
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
__slots__ = ("_buffer", "_capacity", "_head", "_tail", "_size", "_lock")
|
|
58
|
+
|
|
59
|
+
def __init__(self, capacity: int, items: list[T] | None = None) -> None:
|
|
60
|
+
if not isinstance(capacity, int) or capacity < 1:
|
|
61
|
+
raise ValueError(f"Capacity must be a positive integer, got {capacity!r}.")
|
|
62
|
+
self._capacity: int = capacity
|
|
63
|
+
self._buffer: list[T | None] = [None] * capacity
|
|
64
|
+
self._head: int = 0
|
|
65
|
+
self._tail: int = 0
|
|
66
|
+
self._size: int = 0
|
|
67
|
+
self._lock: StructureLock = StructureLock()
|
|
68
|
+
if items is not None:
|
|
69
|
+
if len(items) > capacity:
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"Initial items ({len(items)}) exceeds capacity ({capacity})."
|
|
72
|
+
)
|
|
73
|
+
for item in items:
|
|
74
|
+
self._enqueue_unsafe(item)
|
|
75
|
+
|
|
76
|
+
# ------------------------------------------------------------------ #
|
|
77
|
+
# Internal helpers #
|
|
78
|
+
# ------------------------------------------------------------------ #
|
|
79
|
+
|
|
80
|
+
def _enqueue_unsafe(self, value: T) -> None:
|
|
81
|
+
"""Enqueue without acquiring the lock or full-check (caller must hold lock)."""
|
|
82
|
+
self._buffer[self._tail] = value
|
|
83
|
+
self._tail = (self._tail + 1) % self._capacity
|
|
84
|
+
self._size += 1
|
|
85
|
+
|
|
86
|
+
def _next_index(self, index: int) -> int:
|
|
87
|
+
"""Return the next index modulo capacity."""
|
|
88
|
+
return (index + 1) % self._capacity
|
|
89
|
+
|
|
90
|
+
def _prev_index(self, index: int) -> int:
|
|
91
|
+
"""Return the previous index modulo capacity."""
|
|
92
|
+
return (index - 1 + self._capacity) % self._capacity
|
|
93
|
+
|
|
94
|
+
# ------------------------------------------------------------------ #
|
|
95
|
+
# Required operations #
|
|
96
|
+
# ------------------------------------------------------------------ #
|
|
97
|
+
|
|
98
|
+
def enqueue(self, value: T) -> None:
|
|
99
|
+
"""
|
|
100
|
+
Add *value* to the rear of the queue.
|
|
101
|
+
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
value:
|
|
105
|
+
The element to enqueue.
|
|
106
|
+
|
|
107
|
+
Raises
|
|
108
|
+
------
|
|
109
|
+
QueueFullError
|
|
110
|
+
If the queue is at capacity.
|
|
111
|
+
"""
|
|
112
|
+
with self._lock:
|
|
113
|
+
if self._size == self._capacity:
|
|
114
|
+
raise QueueFullError(self._capacity)
|
|
115
|
+
self._enqueue_unsafe(value)
|
|
116
|
+
|
|
117
|
+
def dequeue(self) -> T:
|
|
118
|
+
"""
|
|
119
|
+
Remove and return the front element.
|
|
120
|
+
|
|
121
|
+
Returns
|
|
122
|
+
-------
|
|
123
|
+
T
|
|
124
|
+
|
|
125
|
+
Raises
|
|
126
|
+
------
|
|
127
|
+
EmptyStructureError
|
|
128
|
+
If the queue is empty.
|
|
129
|
+
"""
|
|
130
|
+
with self._lock:
|
|
131
|
+
if self._size == 0:
|
|
132
|
+
raise EmptyStructureError("dequeue from an empty queue")
|
|
133
|
+
value = self._buffer[self._head]
|
|
134
|
+
self._buffer[self._head] = None
|
|
135
|
+
self._head = self._next_index(self._head)
|
|
136
|
+
self._size -= 1
|
|
137
|
+
return value # type: ignore[return-value]
|
|
138
|
+
|
|
139
|
+
def front(self) -> T:
|
|
140
|
+
"""
|
|
141
|
+
Return the front element without removing it.
|
|
142
|
+
|
|
143
|
+
Returns
|
|
144
|
+
-------
|
|
145
|
+
T
|
|
146
|
+
|
|
147
|
+
Raises
|
|
148
|
+
------
|
|
149
|
+
EmptyStructureError
|
|
150
|
+
If the queue is empty.
|
|
151
|
+
"""
|
|
152
|
+
with self._lock:
|
|
153
|
+
if self._size == 0:
|
|
154
|
+
raise EmptyStructureError("front of an empty queue")
|
|
155
|
+
return self._buffer[self._head] # type: ignore[return-value]
|
|
156
|
+
|
|
157
|
+
def rear(self) -> T:
|
|
158
|
+
"""
|
|
159
|
+
Return the rear element without removing it.
|
|
160
|
+
|
|
161
|
+
Returns
|
|
162
|
+
-------
|
|
163
|
+
T
|
|
164
|
+
|
|
165
|
+
Raises
|
|
166
|
+
------
|
|
167
|
+
EmptyStructureError
|
|
168
|
+
If the queue is empty.
|
|
169
|
+
"""
|
|
170
|
+
with self._lock:
|
|
171
|
+
if self._size == 0:
|
|
172
|
+
raise EmptyStructureError("rear of an empty queue")
|
|
173
|
+
return self._buffer[self._prev_index(self._tail)] # type: ignore[return-value]
|
|
174
|
+
|
|
175
|
+
# ------------------------------------------------------------------ #
|
|
176
|
+
# Query operations #
|
|
177
|
+
# ------------------------------------------------------------------ #
|
|
178
|
+
|
|
179
|
+
def is_empty(self) -> bool:
|
|
180
|
+
"""Return ``True`` if the queue contains no elements."""
|
|
181
|
+
return self._size == 0
|
|
182
|
+
|
|
183
|
+
def is_full(self) -> bool:
|
|
184
|
+
"""Return ``True`` if the queue is at capacity."""
|
|
185
|
+
return self._size == self._capacity
|
|
186
|
+
|
|
187
|
+
def size(self) -> int:
|
|
188
|
+
"""Return the number of elements in the queue."""
|
|
189
|
+
return self._size
|
|
190
|
+
|
|
191
|
+
def capacity(self) -> int:
|
|
192
|
+
"""Return the maximum number of elements the queue can hold."""
|
|
193
|
+
return self._capacity
|
|
194
|
+
|
|
195
|
+
def clear(self) -> None:
|
|
196
|
+
"""Remove all elements from the queue."""
|
|
197
|
+
with self._lock:
|
|
198
|
+
self._buffer = [None] * self._capacity
|
|
199
|
+
self._head = 0
|
|
200
|
+
self._tail = 0
|
|
201
|
+
self._size = 0
|
|
202
|
+
|
|
203
|
+
def copy(self) -> CircularQueue[T]:
|
|
204
|
+
"""
|
|
205
|
+
Return a shallow copy of this queue.
|
|
206
|
+
|
|
207
|
+
Returns
|
|
208
|
+
-------
|
|
209
|
+
CircularQueue[T]
|
|
210
|
+
"""
|
|
211
|
+
with self._lock:
|
|
212
|
+
new = CircularQueue[T](self._capacity)
|
|
213
|
+
new._buffer = list(self._buffer)
|
|
214
|
+
new._head = self._head
|
|
215
|
+
new._tail = self._tail
|
|
216
|
+
new._size = self._size
|
|
217
|
+
return new
|
|
218
|
+
|
|
219
|
+
def to_list(self) -> list[T]:
|
|
220
|
+
"""
|
|
221
|
+
Return a list of all elements, from front to rear.
|
|
222
|
+
|
|
223
|
+
Returns
|
|
224
|
+
-------
|
|
225
|
+
list[T]
|
|
226
|
+
"""
|
|
227
|
+
with self._lock:
|
|
228
|
+
result: list[T] = []
|
|
229
|
+
idx = self._head
|
|
230
|
+
for _ in range(self._size):
|
|
231
|
+
val = self._buffer[idx]
|
|
232
|
+
if val is not None:
|
|
233
|
+
result.append(val)
|
|
234
|
+
idx = self._next_index(idx)
|
|
235
|
+
return result
|
|
236
|
+
|
|
237
|
+
# ------------------------------------------------------------------ #
|
|
238
|
+
# Dunder methods #
|
|
239
|
+
# ------------------------------------------------------------------ #
|
|
240
|
+
|
|
241
|
+
def __iter__(self) -> Iterator[T]:
|
|
242
|
+
with self._lock:
|
|
243
|
+
snapshot = self.to_list()
|
|
244
|
+
return iter(snapshot)
|
|
245
|
+
|
|
246
|
+
def __len__(self) -> int:
|
|
247
|
+
return self._size
|
|
248
|
+
|
|
249
|
+
def __bool__(self) -> bool:
|
|
250
|
+
return self._size > 0
|
|
251
|
+
|
|
252
|
+
def __repr__(self) -> str:
|
|
253
|
+
return f"CircularQueue(capacity={self._capacity}, items={self.to_list()!r})"
|
|
254
|
+
|
|
255
|
+
def __eq__(self, other: object) -> bool:
|
|
256
|
+
if not isinstance(other, CircularQueue):
|
|
257
|
+
return NotImplemented
|
|
258
|
+
return self.to_list() == other.to_list()
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pkstruct.linear.queues.linked_queue
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
A FIFO queue backed by a ``SinglyLinkedList``.
|
|
5
|
+
|
|
6
|
+
``LinkedQueue`` composes the existing production-grade linked list. Enqueue
|
|
7
|
+
appends at the tail; dequeue removes from the head — both O(1).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Iterator
|
|
13
|
+
from typing import TypeVar, cast
|
|
14
|
+
|
|
15
|
+
from pkstruct.linear.exceptions import EmptyStructureError
|
|
16
|
+
from pkstruct.linear.linked_lists.singly_linked_list import SinglyLinkedList
|
|
17
|
+
from pkstruct.linear.queues.queue import Queue
|
|
18
|
+
from pkstruct.shared.threading import StructureLock
|
|
19
|
+
|
|
20
|
+
T = TypeVar("T")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LinkedQueue(Queue[T]):
|
|
24
|
+
"""
|
|
25
|
+
A thread-safe FIFO queue implemented on top of ``SinglyLinkedList``.
|
|
26
|
+
|
|
27
|
+
The internal list stores elements in order: front at the head, rear at
|
|
28
|
+
the tail. ``enqueue`` appends (tail), ``dequeue`` removes from the head
|
|
29
|
+
— both O(1) amortised.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
items:
|
|
34
|
+
Optional iterable of initial values (front-to-rear order).
|
|
35
|
+
If omitted the queue starts empty.
|
|
36
|
+
|
|
37
|
+
Examples
|
|
38
|
+
--------
|
|
39
|
+
>>> q = LinkedQueue([1, 2, 3])
|
|
40
|
+
>>> q.enqueue(4)
|
|
41
|
+
>>> q.dequeue()
|
|
42
|
+
1
|
|
43
|
+
>>> q.front()
|
|
44
|
+
2
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
__slots__ = ("_list", "_lock")
|
|
48
|
+
|
|
49
|
+
def __init__(self, items: list[T] | None = None) -> None:
|
|
50
|
+
self._list: SinglyLinkedList[T] = SinglyLinkedList()
|
|
51
|
+
self._lock: StructureLock = StructureLock()
|
|
52
|
+
if items is not None:
|
|
53
|
+
for item in items:
|
|
54
|
+
self._list.insert(item)
|
|
55
|
+
|
|
56
|
+
# ------------------------------------------------------------------ #
|
|
57
|
+
# Required operations #
|
|
58
|
+
# ------------------------------------------------------------------ #
|
|
59
|
+
|
|
60
|
+
def enqueue(self, value: T) -> None:
|
|
61
|
+
"""
|
|
62
|
+
Add *value* to the rear of the queue (tail of the list).
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
value:
|
|
67
|
+
The element to enqueue.
|
|
68
|
+
"""
|
|
69
|
+
with self._lock:
|
|
70
|
+
self._list.insert(value)
|
|
71
|
+
|
|
72
|
+
def dequeue(self) -> T:
|
|
73
|
+
"""
|
|
74
|
+
Remove and return the front element (head of the list).
|
|
75
|
+
|
|
76
|
+
Returns
|
|
77
|
+
-------
|
|
78
|
+
T
|
|
79
|
+
|
|
80
|
+
Raises
|
|
81
|
+
------
|
|
82
|
+
EmptyStructureError
|
|
83
|
+
If the queue is empty.
|
|
84
|
+
"""
|
|
85
|
+
with self._lock:
|
|
86
|
+
if self._list.is_empty():
|
|
87
|
+
raise EmptyStructureError("dequeue from an empty queue")
|
|
88
|
+
return cast(T, self._list.delete(position=0))
|
|
89
|
+
|
|
90
|
+
def front(self) -> T:
|
|
91
|
+
"""
|
|
92
|
+
Return the front element without removing it.
|
|
93
|
+
|
|
94
|
+
Returns
|
|
95
|
+
-------
|
|
96
|
+
T
|
|
97
|
+
|
|
98
|
+
Raises
|
|
99
|
+
------
|
|
100
|
+
EmptyStructureError
|
|
101
|
+
If the queue is empty.
|
|
102
|
+
"""
|
|
103
|
+
with self._lock:
|
|
104
|
+
if self._list.is_empty():
|
|
105
|
+
raise EmptyStructureError("front of an empty queue")
|
|
106
|
+
return self._list.get(0)
|
|
107
|
+
|
|
108
|
+
def rear(self) -> T:
|
|
109
|
+
"""
|
|
110
|
+
Return the rear element without removing it.
|
|
111
|
+
|
|
112
|
+
Returns
|
|
113
|
+
-------
|
|
114
|
+
T
|
|
115
|
+
|
|
116
|
+
Raises
|
|
117
|
+
------
|
|
118
|
+
EmptyStructureError
|
|
119
|
+
If the queue is empty.
|
|
120
|
+
"""
|
|
121
|
+
with self._lock:
|
|
122
|
+
if self._list.is_empty():
|
|
123
|
+
raise EmptyStructureError("rear of an empty queue")
|
|
124
|
+
return self._list.get(self._list.size() - 1)
|
|
125
|
+
|
|
126
|
+
# ------------------------------------------------------------------ #
|
|
127
|
+
# Query operations #
|
|
128
|
+
# ------------------------------------------------------------------ #
|
|
129
|
+
|
|
130
|
+
def is_empty(self) -> bool:
|
|
131
|
+
"""Return ``True`` if the queue contains no elements."""
|
|
132
|
+
return self._list.is_empty()
|
|
133
|
+
|
|
134
|
+
def size(self) -> int:
|
|
135
|
+
"""Return the number of elements in the queue."""
|
|
136
|
+
return self._list.size()
|
|
137
|
+
|
|
138
|
+
def clear(self) -> None:
|
|
139
|
+
"""Remove all elements from the queue."""
|
|
140
|
+
self._list.clear()
|
|
141
|
+
|
|
142
|
+
def copy(self) -> LinkedQueue[T]:
|
|
143
|
+
"""
|
|
144
|
+
Return a shallow copy of this queue.
|
|
145
|
+
|
|
146
|
+
Returns
|
|
147
|
+
-------
|
|
148
|
+
LinkedQueue[T]
|
|
149
|
+
"""
|
|
150
|
+
with self._lock:
|
|
151
|
+
new = LinkedQueue[T]()
|
|
152
|
+
new._list = cast(SinglyLinkedList[T], self._list.copy())
|
|
153
|
+
return new
|
|
154
|
+
|
|
155
|
+
def to_list(self) -> list[T]:
|
|
156
|
+
"""
|
|
157
|
+
Return a list of all elements, from front to rear.
|
|
158
|
+
|
|
159
|
+
Returns
|
|
160
|
+
-------
|
|
161
|
+
list[T]
|
|
162
|
+
"""
|
|
163
|
+
return self._list.to_list()
|
|
164
|
+
|
|
165
|
+
# ------------------------------------------------------------------ #
|
|
166
|
+
# Dunder methods #
|
|
167
|
+
# ------------------------------------------------------------------ #
|
|
168
|
+
|
|
169
|
+
def __iter__(self) -> Iterator[T]:
|
|
170
|
+
with self._lock:
|
|
171
|
+
snapshot = self.to_list()
|
|
172
|
+
return iter(snapshot)
|
|
173
|
+
|
|
174
|
+
def __len__(self) -> int:
|
|
175
|
+
return self.size()
|
|
176
|
+
|
|
177
|
+
def __bool__(self) -> bool:
|
|
178
|
+
return not self.is_empty()
|
|
179
|
+
|
|
180
|
+
def __repr__(self) -> str:
|
|
181
|
+
return f"LinkedQueue({self.to_list()!r})"
|
|
182
|
+
|
|
183
|
+
def __eq__(self, other: object) -> bool:
|
|
184
|
+
if not isinstance(other, LinkedQueue):
|
|
185
|
+
return NotImplemented
|
|
186
|
+
return self.to_list() == other.to_list()
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pkstruct.linear.queues.priority_queue
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
A priority queue backed by a binary heap (``heapq``).
|
|
5
|
+
|
|
6
|
+
``PriorityQueue`` wraps the standard library ``heapq`` module, adding thread
|
|
7
|
+
safety and the full pkstruct queue interface. The minimum element is always
|
|
8
|
+
at the front. Complexity is O(log n) for ``enqueue`` and ``dequeue``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import heapq
|
|
14
|
+
from collections.abc import Iterator
|
|
15
|
+
from typing import TypeVar
|
|
16
|
+
|
|
17
|
+
from pkstruct.linear.exceptions import EmptyStructureError
|
|
18
|
+
from pkstruct.linear.queues.queue import Queue
|
|
19
|
+
from pkstruct.shared.threading import StructureLock
|
|
20
|
+
|
|
21
|
+
T = TypeVar("T")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PriorityQueue(Queue[T]):
|
|
25
|
+
"""
|
|
26
|
+
A thread-safe min-heap priority queue.
|
|
27
|
+
|
|
28
|
+
The element with the smallest priority (or smallest value when no explicit
|
|
29
|
+
priority is given) is always at the front. If two elements share the same
|
|
30
|
+
priority, the one enqueued first is served first (stable via insertion
|
|
31
|
+
counter).
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
items:
|
|
36
|
+
Optional iterable of initial values. All elements are heapified at
|
|
37
|
+
construction (O(n)).
|
|
38
|
+
|
|
39
|
+
Examples
|
|
40
|
+
--------
|
|
41
|
+
>>> pq = PriorityQueue([3, 1, 2])
|
|
42
|
+
>>> pq.dequeue()
|
|
43
|
+
1
|
|
44
|
+
>>> pq.enqueue(0)
|
|
45
|
+
>>> pq.dequeue()
|
|
46
|
+
0
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
__slots__ = ("_heap", "_lock", "_counter")
|
|
50
|
+
|
|
51
|
+
def __init__(self, items: list[T] | None = None) -> None:
|
|
52
|
+
self._heap: list[tuple[object, int, T]] = []
|
|
53
|
+
self._lock: StructureLock = StructureLock()
|
|
54
|
+
self._counter: int = 0
|
|
55
|
+
if items is not None:
|
|
56
|
+
for item in items:
|
|
57
|
+
self.enqueue(item)
|
|
58
|
+
|
|
59
|
+
# ------------------------------------------------------------------ #
|
|
60
|
+
# Required operations #
|
|
61
|
+
# ------------------------------------------------------------------ #
|
|
62
|
+
|
|
63
|
+
def enqueue(self, value: T, priority: object = None) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Add *value* to the priority queue.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
value:
|
|
70
|
+
The element to enqueue.
|
|
71
|
+
priority:
|
|
72
|
+
Optional sort key. If ``None``, *value* itself is used for
|
|
73
|
+
ordering (requires comparable elements).
|
|
74
|
+
"""
|
|
75
|
+
with self._lock:
|
|
76
|
+
key: object = priority if priority is not None else value
|
|
77
|
+
heapq.heappush(self._heap, (key, self._counter, value))
|
|
78
|
+
self._counter += 1
|
|
79
|
+
|
|
80
|
+
def dequeue(self) -> T:
|
|
81
|
+
"""
|
|
82
|
+
Remove and return the element with the smallest priority.
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
T
|
|
87
|
+
|
|
88
|
+
Raises
|
|
89
|
+
------
|
|
90
|
+
EmptyStructureError
|
|
91
|
+
If the priority queue is empty.
|
|
92
|
+
"""
|
|
93
|
+
with self._lock:
|
|
94
|
+
if not self._heap:
|
|
95
|
+
raise EmptyStructureError("dequeue from an empty priority queue")
|
|
96
|
+
_, _, value = heapq.heappop(self._heap)
|
|
97
|
+
return value
|
|
98
|
+
|
|
99
|
+
def front(self) -> T:
|
|
100
|
+
"""
|
|
101
|
+
Return the smallest-priority element without removing it.
|
|
102
|
+
|
|
103
|
+
Returns
|
|
104
|
+
-------
|
|
105
|
+
T
|
|
106
|
+
|
|
107
|
+
Raises
|
|
108
|
+
------
|
|
109
|
+
EmptyStructureError
|
|
110
|
+
If the priority queue is empty.
|
|
111
|
+
"""
|
|
112
|
+
with self._lock:
|
|
113
|
+
if not self._heap:
|
|
114
|
+
raise EmptyStructureError("front of an empty priority queue")
|
|
115
|
+
return self._heap[0][2]
|
|
116
|
+
|
|
117
|
+
def rear(self) -> T:
|
|
118
|
+
"""
|
|
119
|
+
Return the largest-priority element without removing it.
|
|
120
|
+
|
|
121
|
+
This operation is O(n) because heap order does not directly expose
|
|
122
|
+
the maximum element.
|
|
123
|
+
|
|
124
|
+
Returns
|
|
125
|
+
-------
|
|
126
|
+
T
|
|
127
|
+
|
|
128
|
+
Raises
|
|
129
|
+
------
|
|
130
|
+
EmptyStructureError
|
|
131
|
+
If the priority queue is empty.
|
|
132
|
+
"""
|
|
133
|
+
with self._lock:
|
|
134
|
+
if not self._heap:
|
|
135
|
+
raise EmptyStructureError("rear of an empty priority queue")
|
|
136
|
+
return max(self._heap, key=lambda x: x[0])[2] # type: ignore[arg-type, return-value]
|
|
137
|
+
|
|
138
|
+
# ------------------------------------------------------------------ #
|
|
139
|
+
# Query operations #
|
|
140
|
+
# ------------------------------------------------------------------ #
|
|
141
|
+
|
|
142
|
+
def is_empty(self) -> bool:
|
|
143
|
+
"""Return ``True`` if the priority queue contains no elements."""
|
|
144
|
+
return len(self._heap) == 0
|
|
145
|
+
|
|
146
|
+
def size(self) -> int:
|
|
147
|
+
"""Return the number of elements in the priority queue."""
|
|
148
|
+
return len(self._heap)
|
|
149
|
+
|
|
150
|
+
def clear(self) -> None:
|
|
151
|
+
"""Remove all elements from the priority queue."""
|
|
152
|
+
with self._lock:
|
|
153
|
+
self._heap.clear()
|
|
154
|
+
self._counter = 0
|
|
155
|
+
|
|
156
|
+
def copy(self) -> PriorityQueue[T]:
|
|
157
|
+
"""
|
|
158
|
+
Return a shallow copy of this priority queue.
|
|
159
|
+
|
|
160
|
+
Returns
|
|
161
|
+
-------
|
|
162
|
+
PriorityQueue[T]
|
|
163
|
+
"""
|
|
164
|
+
with self._lock:
|
|
165
|
+
new: PriorityQueue[T] = PriorityQueue()
|
|
166
|
+
new._heap = list(self._heap)
|
|
167
|
+
new._counter = self._counter
|
|
168
|
+
return new
|
|
169
|
+
|
|
170
|
+
def to_list(self) -> list[T]:
|
|
171
|
+
"""
|
|
172
|
+
Return a list of all elements in priority order (smallest first).
|
|
173
|
+
|
|
174
|
+
Returns
|
|
175
|
+
-------
|
|
176
|
+
list[T]
|
|
177
|
+
"""
|
|
178
|
+
with self._lock:
|
|
179
|
+
return [v for _, _, v in sorted(self._heap)]
|
|
180
|
+
|
|
181
|
+
# ------------------------------------------------------------------ #
|
|
182
|
+
# Dunder methods #
|
|
183
|
+
# ------------------------------------------------------------------ #
|
|
184
|
+
|
|
185
|
+
def __iter__(self) -> Iterator[T]:
|
|
186
|
+
with self._lock:
|
|
187
|
+
snapshot = self.to_list()
|
|
188
|
+
return iter(snapshot)
|
|
189
|
+
|
|
190
|
+
def __len__(self) -> int:
|
|
191
|
+
return self.size()
|
|
192
|
+
|
|
193
|
+
def __bool__(self) -> bool:
|
|
194
|
+
return not self.is_empty()
|
|
195
|
+
|
|
196
|
+
def __repr__(self) -> str:
|
|
197
|
+
return f"PriorityQueue({self.to_list()!r})"
|
|
198
|
+
|
|
199
|
+
def __eq__(self, other: object) -> bool:
|
|
200
|
+
if not isinstance(other, PriorityQueue):
|
|
201
|
+
return NotImplemented
|
|
202
|
+
return self.to_list() == other.to_list()
|