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.
Files changed (72) hide show
  1. pkstruct/__init__.py +167 -0
  2. pkstruct/graphs/__init__.py +127 -0
  3. pkstruct/graphs/connectivity.py +157 -0
  4. pkstruct/graphs/directed.py +95 -0
  5. pkstruct/graphs/exceptions.py +63 -0
  6. pkstruct/graphs/graph.py +262 -0
  7. pkstruct/graphs/mst.py +118 -0
  8. pkstruct/graphs/scc.py +138 -0
  9. pkstruct/graphs/shortest_path.py +250 -0
  10. pkstruct/graphs/topo_sort.py +108 -0
  11. pkstruct/graphs/traversal.py +175 -0
  12. pkstruct/graphs/visualization.py +90 -0
  13. pkstruct/graphs/weighted.py +37 -0
  14. pkstruct/linear/__init__.py +95 -0
  15. pkstruct/linear/deques/__init__.py +33 -0
  16. pkstruct/linear/deques/deque.py +194 -0
  17. pkstruct/linear/deques/linked_deque.py +198 -0
  18. pkstruct/linear/exceptions.py +26 -0
  19. pkstruct/linear/linked_lists/__init__.py +5 -0
  20. pkstruct/linear/linked_lists/_base.py +608 -0
  21. pkstruct/linear/linked_lists/circular_linked_list.py +230 -0
  22. pkstruct/linear/linked_lists/doubly_linked_list.py +151 -0
  23. pkstruct/linear/linked_lists/nodes.py +68 -0
  24. pkstruct/linear/linked_lists/singly_linked_list.py +136 -0
  25. pkstruct/linear/queues/__init__.py +44 -0
  26. pkstruct/linear/queues/circular_queue.py +258 -0
  27. pkstruct/linear/queues/linked_queue.py +186 -0
  28. pkstruct/linear/queues/priority_queue.py +202 -0
  29. pkstruct/linear/queues/queue.py +174 -0
  30. pkstruct/linear/stacks/__init__.py +38 -0
  31. pkstruct/linear/stacks/array_stack.py +165 -0
  32. pkstruct/linear/stacks/linked_stack.py +168 -0
  33. pkstruct/linear/stacks/stack.py +158 -0
  34. pkstruct/linear/utils/__init__.py +18 -0
  35. pkstruct/linear/utils/benchmark.py +255 -0
  36. pkstruct/linear/utils/debug_tools.py +239 -0
  37. pkstruct/linear/utils/helpers.py +143 -0
  38. pkstruct/linear/utils/iterators.py +148 -0
  39. pkstruct/linear/visualization/__init__.py +0 -0
  40. pkstruct/linear/visualization/ascii_visualizer.py +114 -0
  41. pkstruct/linear/visualization/linked_list_visualizer.py +126 -0
  42. pkstruct/shared/__init__.py +67 -0
  43. pkstruct/shared/benchmarking/__init__.py +78 -0
  44. pkstruct/shared/debugging/__init__.py +69 -0
  45. pkstruct/shared/exceptions/__init__.py +59 -0
  46. pkstruct/shared/serializers/__init__.py +65 -0
  47. pkstruct/shared/threading/__init__.py +43 -0
  48. pkstruct/shared/validators/__init__.py +98 -0
  49. pkstruct/shared/visualization/__init__.py +21 -0
  50. pkstruct/trees/__init__.py +92 -0
  51. pkstruct/trees/avl.py +321 -0
  52. pkstruct/trees/balancing.py +253 -0
  53. pkstruct/trees/bplus.py +425 -0
  54. pkstruct/trees/bst.py +948 -0
  55. pkstruct/trees/btree.py +504 -0
  56. pkstruct/trees/exceptions.py +96 -0
  57. pkstruct/trees/fenwick_tree.py +312 -0
  58. pkstruct/trees/interval_tree.py +541 -0
  59. pkstruct/trees/node.py +356 -0
  60. pkstruct/trees/red_black.py +710 -0
  61. pkstruct/trees/segment_tree.py +398 -0
  62. pkstruct/trees/traversal.py +456 -0
  63. pkstruct/trees/tree_helpers.py +366 -0
  64. pkstruct/trees/utils/__init__.py +15 -0
  65. pkstruct/trees/utils/complexity_helpers.py +231 -0
  66. pkstruct/trees/visualization/__init__.py +0 -0
  67. pkstruct/trees/visualization/ascii_renderer.py +220 -0
  68. pkstruct/trees/visualization/tree_printer.py +129 -0
  69. pkstruct-0.1.0.dist-info/METADATA +482 -0
  70. pkstruct-0.1.0.dist-info/RECORD +72 -0
  71. pkstruct-0.1.0.dist-info/WHEEL +4 -0
  72. 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)