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,18 @@
|
|
|
1
|
+
from .benchmark import benchmark_operations, compare_with_builtins
|
|
2
|
+
from .debug_tools import memory_usage, validate_integrity
|
|
3
|
+
from .helpers import detect_intersection, list_equal, merge_sorted_lists, to_array
|
|
4
|
+
from .iterators import BackwardIterator, CircularIterator, ForwardIterator
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"ForwardIterator",
|
|
8
|
+
"BackwardIterator",
|
|
9
|
+
"CircularIterator",
|
|
10
|
+
"benchmark_operations",
|
|
11
|
+
"compare_with_builtins",
|
|
12
|
+
"merge_sorted_lists",
|
|
13
|
+
"detect_intersection",
|
|
14
|
+
"list_equal",
|
|
15
|
+
"to_array",
|
|
16
|
+
"memory_usage",
|
|
17
|
+
"validate_integrity",
|
|
18
|
+
]
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Benchmarking utilities for pkstruct linked list classes."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import collections
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pkstruct.linear import CircularLinkedList, DoublyLinkedList, SinglyLinkedList
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _timeit(fn, *args, **kwargs) -> float:
|
|
12
|
+
"""Run *fn* once and return elapsed seconds."""
|
|
13
|
+
start = time.perf_counter()
|
|
14
|
+
fn(*args, **kwargs)
|
|
15
|
+
return time.perf_counter() - start
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def benchmark_operations(
|
|
19
|
+
list_class: type,
|
|
20
|
+
sizes: list[int] | None = None,
|
|
21
|
+
n: int | None = None,
|
|
22
|
+
) -> dict[str, Any]:
|
|
23
|
+
"""Time insert / delete / get / search operations for *list_class*.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
list_class: A pkstruct linked list class to benchmark.
|
|
27
|
+
sizes: List of element counts to test against.
|
|
28
|
+
Defaults to ``[100, 1000, 10000]``.
|
|
29
|
+
n: If provided, sets sizes to [n].
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Nested dict::
|
|
33
|
+
|
|
34
|
+
{
|
|
35
|
+
size: {
|
|
36
|
+
"insert_head": float, # seconds
|
|
37
|
+
"insert_tail": float,
|
|
38
|
+
"delete_head": float,
|
|
39
|
+
"delete_tail": float,
|
|
40
|
+
"get_middle": float,
|
|
41
|
+
"search": float,
|
|
42
|
+
},
|
|
43
|
+
...
|
|
44
|
+
}
|
|
45
|
+
"""
|
|
46
|
+
if n is not None:
|
|
47
|
+
sizes = [n]
|
|
48
|
+
if sizes is None:
|
|
49
|
+
sizes = [100, 1000, 10000]
|
|
50
|
+
|
|
51
|
+
results: dict[str, Any] = {}
|
|
52
|
+
|
|
53
|
+
for n in sizes:
|
|
54
|
+
data = list(range(n))
|
|
55
|
+
stats: dict[str, float] = {}
|
|
56
|
+
|
|
57
|
+
# --- insert head (position 0) ---
|
|
58
|
+
lst = list_class()
|
|
59
|
+
start = time.perf_counter()
|
|
60
|
+
for v in data:
|
|
61
|
+
lst.insert(v, position=0)
|
|
62
|
+
stats["insert_head"] = time.perf_counter() - start
|
|
63
|
+
|
|
64
|
+
# --- insert tail (position=None appends) ---
|
|
65
|
+
lst2 = list_class()
|
|
66
|
+
start = time.perf_counter()
|
|
67
|
+
for v in data:
|
|
68
|
+
lst2.insert(v) # position=None appends at tail
|
|
69
|
+
stats["insert_tail"] = time.perf_counter() - start
|
|
70
|
+
|
|
71
|
+
# Build a fresh list for deletion / access tests
|
|
72
|
+
lst3 = list_class.from_list(data)
|
|
73
|
+
|
|
74
|
+
# --- get middle ---
|
|
75
|
+
mid = n // 2
|
|
76
|
+
start = time.perf_counter()
|
|
77
|
+
_ = lst3.get(mid)
|
|
78
|
+
stats["get_middle"] = time.perf_counter() - start
|
|
79
|
+
|
|
80
|
+
# --- search using index() ---
|
|
81
|
+
target = data[mid]
|
|
82
|
+
start = time.perf_counter()
|
|
83
|
+
_ = lst3.index(target)
|
|
84
|
+
stats["search"] = time.perf_counter() - start
|
|
85
|
+
|
|
86
|
+
# --- delete head (position 0) ---
|
|
87
|
+
lst4 = list_class.from_list(data)
|
|
88
|
+
start = time.perf_counter()
|
|
89
|
+
for _ in range(min(100, n)):
|
|
90
|
+
lst4.delete(position=0)
|
|
91
|
+
stats["delete_head"] = time.perf_counter() - start
|
|
92
|
+
|
|
93
|
+
# --- delete tail (position = size-1) ---
|
|
94
|
+
lst5 = list_class.from_list(data)
|
|
95
|
+
start = time.perf_counter()
|
|
96
|
+
for _ in range(min(100, n)):
|
|
97
|
+
lst5.delete(position=lst5.size() - 1)
|
|
98
|
+
stats["delete_tail"] = time.perf_counter() - start
|
|
99
|
+
|
|
100
|
+
results[str(n)] = stats
|
|
101
|
+
|
|
102
|
+
return results
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def compare_with_builtins(sizes: list[int] | None = None, n: int | None = None) -> dict[str, Any]:
|
|
106
|
+
"""Compare all pkstruct list types against Python ``list`` and ``deque``.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
sizes: List of element counts to test. Defaults to ``[100, 1000]``.
|
|
110
|
+
n: If provided, sets sizes to [n].
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Nested dict::
|
|
114
|
+
|
|
115
|
+
{
|
|
116
|
+
size: {
|
|
117
|
+
class_name: {operation: seconds, ...},
|
|
118
|
+
...
|
|
119
|
+
},
|
|
120
|
+
...
|
|
121
|
+
}
|
|
122
|
+
"""
|
|
123
|
+
if n is not None:
|
|
124
|
+
sizes = [n]
|
|
125
|
+
if sizes is None:
|
|
126
|
+
sizes = [100, 1000]
|
|
127
|
+
|
|
128
|
+
from pkstruct.linear.linked_lists.circular_linked_list import CircularLinkedList
|
|
129
|
+
from pkstruct.linear.linked_lists.doubly_linked_list import DoublyLinkedList
|
|
130
|
+
from pkstruct.linear.linked_lists.singly_linked_list import SinglyLinkedList
|
|
131
|
+
|
|
132
|
+
pkstruct_classes = [SinglyLinkedList, DoublyLinkedList, CircularLinkedList]
|
|
133
|
+
|
|
134
|
+
results: dict[str, Any] = {}
|
|
135
|
+
|
|
136
|
+
for n in sizes:
|
|
137
|
+
data = list(range(n))
|
|
138
|
+
size_results: dict[str, dict[str, float]] = {}
|
|
139
|
+
|
|
140
|
+
# --- pkstruct classes ---
|
|
141
|
+
for cls in pkstruct_classes:
|
|
142
|
+
name = cls.__name__
|
|
143
|
+
stats: dict[str, float] = {}
|
|
144
|
+
|
|
145
|
+
# Insert tail
|
|
146
|
+
lst = cls()
|
|
147
|
+
start = time.perf_counter()
|
|
148
|
+
for v in data:
|
|
149
|
+
lst.insert(v) # position=None appends
|
|
150
|
+
stats["insert_tail"] = time.perf_counter() - start
|
|
151
|
+
|
|
152
|
+
# Search using index
|
|
153
|
+
start = time.perf_counter()
|
|
154
|
+
_ = lst.index(data[n // 2])
|
|
155
|
+
stats["search"] = time.perf_counter() - start
|
|
156
|
+
|
|
157
|
+
# Delete head
|
|
158
|
+
stats["delete_head"] = _timeit(lst.delete, position=0)
|
|
159
|
+
|
|
160
|
+
size_results[name] = stats
|
|
161
|
+
|
|
162
|
+
# --- Python list ---
|
|
163
|
+
py_stats: dict[str, float] = {}
|
|
164
|
+
py_list: list[int] = []
|
|
165
|
+
|
|
166
|
+
# Insert tail
|
|
167
|
+
start = time.perf_counter()
|
|
168
|
+
for v in data:
|
|
169
|
+
py_list.append(v)
|
|
170
|
+
py_stats["insert_tail"] = time.perf_counter() - start
|
|
171
|
+
|
|
172
|
+
# Search
|
|
173
|
+
start = time.perf_counter()
|
|
174
|
+
_ = (data[n // 2] in py_list)
|
|
175
|
+
py_stats["search"] = time.perf_counter() - start
|
|
176
|
+
|
|
177
|
+
# Delete head
|
|
178
|
+
start = time.perf_counter()
|
|
179
|
+
if py_list:
|
|
180
|
+
py_list.pop(0)
|
|
181
|
+
py_stats["delete_head"] = time.perf_counter() - start
|
|
182
|
+
|
|
183
|
+
size_results["list"] = py_stats
|
|
184
|
+
|
|
185
|
+
# --- collections.deque ---
|
|
186
|
+
dq_stats: dict[str, float] = {}
|
|
187
|
+
dq: collections.deque[int] = collections.deque()
|
|
188
|
+
|
|
189
|
+
# Insert tail
|
|
190
|
+
start = time.perf_counter()
|
|
191
|
+
for v in data:
|
|
192
|
+
dq.append(v)
|
|
193
|
+
dq_stats["insert_tail"] = time.perf_counter() - start
|
|
194
|
+
|
|
195
|
+
# Search (deque requires iteration, so convert to list or use 'in')
|
|
196
|
+
start = time.perf_counter()
|
|
197
|
+
_ = (data[n // 2] in dq)
|
|
198
|
+
dq_stats["search"] = time.perf_counter() - start
|
|
199
|
+
|
|
200
|
+
# Delete head
|
|
201
|
+
start = time.perf_counter()
|
|
202
|
+
if dq:
|
|
203
|
+
dq.popleft()
|
|
204
|
+
dq_stats["delete_head"] = time.perf_counter() - start
|
|
205
|
+
|
|
206
|
+
size_results["deque"] = dq_stats
|
|
207
|
+
|
|
208
|
+
results[str(n)] = size_results
|
|
209
|
+
|
|
210
|
+
return results
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def run_full_benchmark() -> dict[str, Any]:
|
|
214
|
+
"""Run complete benchmark suite and return results."""
|
|
215
|
+
print("Running pkstruct benchmarks...")
|
|
216
|
+
|
|
217
|
+
print("\n--- Benchmark: SinglyLinkedList ---")
|
|
218
|
+
singly_results = benchmark_operations(
|
|
219
|
+
SinglyLinkedList,
|
|
220
|
+
sizes=[100, 1000, 5000]
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
print("\n--- Benchmark: DoublyLinkedList ---")
|
|
224
|
+
doubly_results = benchmark_operations(
|
|
225
|
+
DoublyLinkedList,
|
|
226
|
+
sizes=[100, 1000, 5000]
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
print("\n--- Benchmark: CircularLinkedList ---")
|
|
230
|
+
circular_results = benchmark_operations(
|
|
231
|
+
CircularLinkedList,
|
|
232
|
+
sizes=[100, 1000, 5000]
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
print("\n--- Comparison with Built-ins ---")
|
|
236
|
+
comparison = compare_with_builtins(sizes=[100, 500])
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
"singly": singly_results,
|
|
240
|
+
"doubly": doubly_results,
|
|
241
|
+
"circular": circular_results,
|
|
242
|
+
"vs_builtins": comparison,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
if __name__ == "__main__":
|
|
247
|
+
# Run benchmarks when script executed directly
|
|
248
|
+
results = run_full_benchmark()
|
|
249
|
+
|
|
250
|
+
# Pretty print results
|
|
251
|
+
import json
|
|
252
|
+
print("\n" + "="*60)
|
|
253
|
+
print("FINAL RESULTS")
|
|
254
|
+
print("="*60)
|
|
255
|
+
print(json.dumps(results, indent=2, default=str))
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Debug and integrity utilities for pkstruct linked lists."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _get_list_types() -> tuple[type, type, type]:
|
|
9
|
+
from pkstruct.linear.linked_lists.circular_linked_list import CircularLinkedList
|
|
10
|
+
from pkstruct.linear.linked_lists.doubly_linked_list import DoublyLinkedList
|
|
11
|
+
from pkstruct.linear.linked_lists.singly_linked_list import SinglyLinkedList
|
|
12
|
+
return SinglyLinkedList, DoublyLinkedList, CircularLinkedList
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Base per-node overhead in bytes (measured experimentally on CPython 3.10)
|
|
16
|
+
_SINGLY_NODE_BYTES = 56
|
|
17
|
+
_DOUBLY_NODE_BYTES = 72
|
|
18
|
+
_CIRCULAR_NODE_BYTES = 56
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def memory_usage(list_instance: Any) -> int:
|
|
22
|
+
"""Estimate memory used by *list_instance* in bytes.
|
|
23
|
+
|
|
24
|
+
Uses fixed per-node overhead constants plus ``sys.getsizeof`` for the
|
|
25
|
+
value stored in each node.
|
|
26
|
+
|
|
27
|
+
Node overhead constants:
|
|
28
|
+
- SinglyNode / CircularNode: 56 bytes
|
|
29
|
+
- DoublyNode: 72 bytes
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
list_instance: Any pkstruct linked list instance.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Estimated total bytes used by the list structure.
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
TypeError: If *list_instance* is not a recognised list type.
|
|
39
|
+
"""
|
|
40
|
+
SinglyLinkedList, DoublyLinkedList, CircularLinkedList = _get_list_types()
|
|
41
|
+
|
|
42
|
+
if isinstance(list_instance, CircularLinkedList):
|
|
43
|
+
per_node = _CIRCULAR_NODE_BYTES
|
|
44
|
+
current = list_instance.head
|
|
45
|
+
size = list_instance.size()
|
|
46
|
+
steps = 0
|
|
47
|
+
total = 0
|
|
48
|
+
while current is not None and steps < size:
|
|
49
|
+
try:
|
|
50
|
+
value_bytes = sys.getsizeof(current.value)
|
|
51
|
+
except Exception:
|
|
52
|
+
value_bytes = 0
|
|
53
|
+
total += per_node + value_bytes
|
|
54
|
+
current = current.next
|
|
55
|
+
steps += 1
|
|
56
|
+
return total
|
|
57
|
+
|
|
58
|
+
if isinstance(list_instance, DoublyLinkedList):
|
|
59
|
+
per_node = _DOUBLY_NODE_BYTES
|
|
60
|
+
elif isinstance(list_instance, SinglyLinkedList):
|
|
61
|
+
per_node = _SINGLY_NODE_BYTES
|
|
62
|
+
else:
|
|
63
|
+
raise TypeError(
|
|
64
|
+
f"memory_usage does not support {type(list_instance).__name__}."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
total = 0
|
|
68
|
+
current = list_instance.head
|
|
69
|
+
visited: set[int] = set()
|
|
70
|
+
while current is not None:
|
|
71
|
+
if id(current) in visited:
|
|
72
|
+
break
|
|
73
|
+
visited.add(id(current))
|
|
74
|
+
try:
|
|
75
|
+
value_bytes = sys.getsizeof(current.value)
|
|
76
|
+
except Exception:
|
|
77
|
+
value_bytes = 0
|
|
78
|
+
total += per_node + value_bytes
|
|
79
|
+
current = current.next
|
|
80
|
+
|
|
81
|
+
return total
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def validate_integrity(list_instance: Any) -> dict[str, Any]:
|
|
85
|
+
"""Validate the structural integrity of *list_instance*.
|
|
86
|
+
|
|
87
|
+
Checks performed:
|
|
88
|
+
- **SinglyLinkedList**: no cycles (via Floyd's algorithm), size matches.
|
|
89
|
+
- **DoublyLinkedList**: all ``prev``/``next`` pointers consistent,
|
|
90
|
+
no cycles, size matches.
|
|
91
|
+
- **CircularLinkedList**: ``tail.next is head``, no broken links,
|
|
92
|
+
size matches.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
list_instance: Any pkstruct linked list instance.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Dictionary::
|
|
99
|
+
|
|
100
|
+
{
|
|
101
|
+
"valid": bool,
|
|
102
|
+
"errors": list[str],
|
|
103
|
+
"size_matches": bool,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
TypeError: If *list_instance* is not a recognised list type.
|
|
108
|
+
"""
|
|
109
|
+
SinglyLinkedList, DoublyLinkedList, CircularLinkedList = _get_list_types()
|
|
110
|
+
|
|
111
|
+
errors: list[str] = []
|
|
112
|
+
size_matches = True
|
|
113
|
+
|
|
114
|
+
if isinstance(list_instance, CircularLinkedList):
|
|
115
|
+
errors, size_matches = _validate_circular(list_instance)
|
|
116
|
+
|
|
117
|
+
elif isinstance(list_instance, DoublyLinkedList):
|
|
118
|
+
errors, size_matches = _validate_doubly(list_instance)
|
|
119
|
+
|
|
120
|
+
elif isinstance(list_instance, SinglyLinkedList):
|
|
121
|
+
errors, size_matches = _validate_singly(list_instance)
|
|
122
|
+
|
|
123
|
+
else:
|
|
124
|
+
raise TypeError(
|
|
125
|
+
f"validate_integrity does not support {type(list_instance).__name__}."
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
"valid": len(errors) == 0,
|
|
130
|
+
"errors": errors,
|
|
131
|
+
"size_matches": size_matches,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _validate_singly(lst: Any) -> tuple[list[str], bool]:
|
|
136
|
+
errors: list[str] = []
|
|
137
|
+
|
|
138
|
+
# Floyd's cycle detection
|
|
139
|
+
slow = lst.head
|
|
140
|
+
fast = lst.head
|
|
141
|
+
while fast is not None and fast.next is not None:
|
|
142
|
+
slow = slow.next
|
|
143
|
+
fast = fast.next.next
|
|
144
|
+
if slow is fast:
|
|
145
|
+
errors.append("Cycle detected in SinglyLinkedList.")
|
|
146
|
+
break
|
|
147
|
+
|
|
148
|
+
# Count nodes
|
|
149
|
+
counted = 0
|
|
150
|
+
current = lst.head
|
|
151
|
+
visited: set[int] = set()
|
|
152
|
+
while current is not None:
|
|
153
|
+
if id(current) in visited:
|
|
154
|
+
break
|
|
155
|
+
visited.add(id(current))
|
|
156
|
+
counted += 1
|
|
157
|
+
current = current.next
|
|
158
|
+
|
|
159
|
+
size_matches = counted == lst.size()
|
|
160
|
+
if not size_matches:
|
|
161
|
+
errors.append(
|
|
162
|
+
f"Size mismatch: recorded={lst.size()}, counted={counted}."
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
return errors, size_matches
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _validate_doubly(lst: Any) -> tuple[list[str], bool]:
|
|
169
|
+
errors: list[str] = []
|
|
170
|
+
|
|
171
|
+
# Walk forward, check prev pointers
|
|
172
|
+
prev_node = None
|
|
173
|
+
current = lst.head
|
|
174
|
+
counted = 0
|
|
175
|
+
visited: set[int] = set()
|
|
176
|
+
|
|
177
|
+
while current is not None:
|
|
178
|
+
if id(current) in visited:
|
|
179
|
+
errors.append(f"Cycle detected at node with data={current.value!r}.")
|
|
180
|
+
break
|
|
181
|
+
visited.add(id(current))
|
|
182
|
+
|
|
183
|
+
if current.prev is not prev_node:
|
|
184
|
+
errors.append(
|
|
185
|
+
f"Broken prev pointer at node data={current.value!r}: "
|
|
186
|
+
f"expected id={id(prev_node)}, got id={id(current.prev)}."
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
prev_node = current
|
|
190
|
+
current = current.next
|
|
191
|
+
counted += 1
|
|
192
|
+
|
|
193
|
+
# Verify tail pointer
|
|
194
|
+
if lst.tail is not None and lst.tail is not prev_node:
|
|
195
|
+
errors.append(
|
|
196
|
+
f"Tail pointer mismatch: tail.data={lst.tail.value!r}, "
|
|
197
|
+
f"last traversed data={prev_node.value if prev_node else None!r}."
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
size_matches = counted == lst.size()
|
|
201
|
+
if not size_matches:
|
|
202
|
+
errors.append(
|
|
203
|
+
f"Size mismatch: recorded={lst.size()}, counted={counted}."
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
return errors, size_matches
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _validate_circular(lst: Any) -> tuple[list[str], bool]:
|
|
210
|
+
errors: list[str] = []
|
|
211
|
+
|
|
212
|
+
# tail.next must be head
|
|
213
|
+
if lst.head is not None and lst.tail is not None:
|
|
214
|
+
if lst.tail.next is not lst.head:
|
|
215
|
+
errors.append(
|
|
216
|
+
"CircularLinkedList integrity error: tail.next is not head."
|
|
217
|
+
)
|
|
218
|
+
elif lst.head is None and lst.tail is not None:
|
|
219
|
+
errors.append("head is None but tail is not None.")
|
|
220
|
+
elif lst.tail is None and lst.head is not None:
|
|
221
|
+
errors.append("tail is None but head is not None.")
|
|
222
|
+
|
|
223
|
+
# Count nodes (bounded by reported size + 1 to detect over-count)
|
|
224
|
+
counted = 0
|
|
225
|
+
limit = lst.size() + 1
|
|
226
|
+
current = lst.head
|
|
227
|
+
while current is not None and counted < limit:
|
|
228
|
+
counted += 1
|
|
229
|
+
current = current.next
|
|
230
|
+
if current is lst.head:
|
|
231
|
+
break
|
|
232
|
+
|
|
233
|
+
size_matches = counted == lst.size()
|
|
234
|
+
if not size_matches:
|
|
235
|
+
errors.append(
|
|
236
|
+
f"Size mismatch: recorded={lst.size()}, counted={counted}."
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
return errors, size_matches
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Helper utilities for pkstruct linked lists."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _get_list_types() -> tuple[type, type, type]:
|
|
8
|
+
from pkstruct.linear.linked_lists.circular_linked_list import CircularLinkedList
|
|
9
|
+
from pkstruct.linear.linked_lists.doubly_linked_list import DoublyLinkedList
|
|
10
|
+
from pkstruct.linear.linked_lists.singly_linked_list import SinglyLinkedList
|
|
11
|
+
return SinglyLinkedList, DoublyLinkedList, CircularLinkedList
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _to_values(lst: Any) -> list[Any]:
|
|
15
|
+
"""Extract all values from a linked list into a Python list."""
|
|
16
|
+
SinglyLinkedList, DoublyLinkedList, CircularLinkedList = _get_list_types()
|
|
17
|
+
|
|
18
|
+
if isinstance(lst, CircularLinkedList):
|
|
19
|
+
result = []
|
|
20
|
+
current = lst.head
|
|
21
|
+
size = lst.size
|
|
22
|
+
steps = 0
|
|
23
|
+
while current is not None and steps < size:
|
|
24
|
+
result.append(current.value) # FIXED: .data → .value
|
|
25
|
+
current = current.next
|
|
26
|
+
steps += 1
|
|
27
|
+
return result
|
|
28
|
+
|
|
29
|
+
result = []
|
|
30
|
+
current = lst.head
|
|
31
|
+
visited: set[int] = set()
|
|
32
|
+
while current is not None:
|
|
33
|
+
if id(current) in visited:
|
|
34
|
+
break
|
|
35
|
+
visited.add(id(current))
|
|
36
|
+
result.append(current.value) # FIXED: .data → .value
|
|
37
|
+
current = current.next
|
|
38
|
+
return result
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def merge_sorted_lists(*lists: Any) -> Any:
|
|
42
|
+
"""Merge pre-sorted linked lists into a new sorted SinglyLinkedList.
|
|
43
|
+
|
|
44
|
+
All input lists must already be sorted in ascending order.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
*lists: Any number of pkstruct linked list instances, each sorted.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
A new SinglyLinkedList containing all elements from *lists* in
|
|
51
|
+
sorted (ascending) order.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
ValueError: If no lists are provided.
|
|
55
|
+
"""
|
|
56
|
+
if not lists:
|
|
57
|
+
raise ValueError("merge_sorted_lists requires at least one list argument.")
|
|
58
|
+
|
|
59
|
+
SinglyLinkedList, _, _ = _get_list_types()
|
|
60
|
+
|
|
61
|
+
# Collect all values, sort, build new list
|
|
62
|
+
all_values: list[Any] = []
|
|
63
|
+
for lst in lists:
|
|
64
|
+
all_values.extend(_to_values(lst))
|
|
65
|
+
|
|
66
|
+
all_values.sort()
|
|
67
|
+
return SinglyLinkedList.from_list(all_values)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def detect_intersection(list_a: Any, list_b: Any) -> Any | None:
|
|
71
|
+
"""Detect whether two linked lists share a common node by identity.
|
|
72
|
+
|
|
73
|
+
Uses a hash-set approach on node ids to find the first node that
|
|
74
|
+
appears in both *list_a* and *list_b*.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
list_a: First linked list instance.
|
|
78
|
+
list_b: Second linked list instance.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
The data value of the first intersecting node, or ``None`` if the
|
|
82
|
+
lists do not intersect.
|
|
83
|
+
"""
|
|
84
|
+
SinglyLinkedList, DoublyLinkedList, CircularLinkedList = _get_list_types()
|
|
85
|
+
|
|
86
|
+
def node_ids(lst: Any) -> dict[int, Any]:
|
|
87
|
+
"""Return mapping of node id → data for *lst*."""
|
|
88
|
+
mapping: dict[int, Any] = {}
|
|
89
|
+
if isinstance(lst, CircularLinkedList):
|
|
90
|
+
current = lst.head
|
|
91
|
+
size = lst.size
|
|
92
|
+
steps = 0
|
|
93
|
+
while current is not None and steps < size:
|
|
94
|
+
mapping[id(current)] = current.value # FIXED: .data → .value
|
|
95
|
+
current = current.next
|
|
96
|
+
steps += 1
|
|
97
|
+
else:
|
|
98
|
+
current = lst.head
|
|
99
|
+
visited: set[int] = set()
|
|
100
|
+
while current is not None:
|
|
101
|
+
nid = id(current)
|
|
102
|
+
if nid in visited:
|
|
103
|
+
break
|
|
104
|
+
visited.add(nid)
|
|
105
|
+
mapping[nid] = current.value # FIXED: .data → .value
|
|
106
|
+
current = current.next
|
|
107
|
+
return mapping
|
|
108
|
+
|
|
109
|
+
ids_a = node_ids(list_a)
|
|
110
|
+
ids_b = node_ids(list_b)
|
|
111
|
+
|
|
112
|
+
for nid in ids_b:
|
|
113
|
+
if nid in ids_a:
|
|
114
|
+
return ids_b[nid]
|
|
115
|
+
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def list_equal(list_a: Any, list_b: Any) -> bool:
|
|
120
|
+
"""Return True if *list_a* and *list_b* contain the same values in order.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
list_a: First linked list instance.
|
|
124
|
+
list_b: Second linked list instance.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
``True`` if the value sequences are identical, ``False`` otherwise.
|
|
128
|
+
"""
|
|
129
|
+
return _to_values(list_a) == _to_values(list_b)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def to_array(list_instance: Any) -> list[Any]:
|
|
133
|
+
"""Alias for converting a linked list to a Python list via ``to_list()``.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
list_instance: Any pkstruct linked list instance.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
A plain Python list of node values in forward order.
|
|
140
|
+
"""
|
|
141
|
+
if hasattr(list_instance, "to_list"):
|
|
142
|
+
return list_instance.to_list()
|
|
143
|
+
return _to_values(list_instance)
|