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,262 @@
1
+ """
2
+ pkstruct.graphs.graph
3
+ =====================
4
+ Core graph data structure using adjacency-list representation.
5
+
6
+ Supports both directed and undirected modes, with optional edge weights.
7
+ All public operations are thread-safe via ``StructureLock``.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections.abc import Iterator
13
+ from typing import Any
14
+
15
+ from pkstruct.graphs.exceptions import VertexNotFoundError, EdgeNotFoundError
16
+ from pkstruct.shared.threading import StructureLock
17
+
18
+
19
+ class Graph:
20
+ """Adjacency-list based graph supporting directed/undirected and weighted modes.
21
+
22
+ Parameters
23
+ ----------
24
+ directed : bool, default=False
25
+ If *True*, edges are one-way; otherwise edges are bidirectional.
26
+
27
+ Example
28
+ -------
29
+ >>> g = Graph()
30
+ >>> g.add_vertex("A")
31
+ >>> g.add_edge("A", "B")
32
+ >>> g.add_edge("A", "C")
33
+ >>> len(g)
34
+ 3
35
+ >>> list(g.get_neighbors("A"))
36
+ ['B', 'C']
37
+ """
38
+
39
+ __slots__ = ("_adj", "_directed", "_lock", "_edge_count")
40
+
41
+ def __init__(self, directed: bool = False) -> None:
42
+ self._adj: dict[Any, dict[Any, float]] = {}
43
+ self._directed: bool = directed
44
+ self._lock: StructureLock = StructureLock()
45
+ self._edge_count: int = 0
46
+
47
+ # ------------------------------------------------------------------
48
+ # Mutation
49
+ # ------------------------------------------------------------------
50
+
51
+ def add_vertex(self, v: Any) -> None:
52
+ """Add a vertex to the graph. No-op if already present."""
53
+ with self._lock:
54
+ if v not in self._adj:
55
+ self._adj[v] = {}
56
+
57
+ def add_edge(self, u: Any, v: Any, weight: float = 1.0) -> None:
58
+ """Add an edge between *u* and *v* with optional *weight*.
59
+
60
+ Vertices are created automatically if they do not exist.
61
+ If the edge already exists, its weight is updated.
62
+ """
63
+ with self._lock:
64
+ self.add_vertex(u)
65
+ self.add_vertex(v)
66
+ if v not in self._adj[u]:
67
+ self._edge_count += 1
68
+ self._adj[u][v] = weight
69
+ if not self._directed and u != v:
70
+ self._adj[v][u] = weight
71
+
72
+ def remove_vertex(self, v: Any) -> None:
73
+ """Remove *v* and all incident edges.
74
+
75
+ Raises
76
+ ------
77
+ VertexNotFoundError
78
+ If *v* is not in the graph.
79
+ """
80
+ with self._lock:
81
+ if v not in self._adj:
82
+ raise VertexNotFoundError(v)
83
+ for neighbor in list(self._adj[v]):
84
+ self._adj[neighbor].pop(v, None)
85
+ del self._adj[v]
86
+ self._edge_count = sum(len(nbrs) for nbrs in self._adj.values())
87
+ if not self._directed:
88
+ self._edge_count //= 2
89
+
90
+ def remove_edge(self, u: Any, v: Any) -> None:
91
+ """Remove the edge from *u* to *v*.
92
+
93
+ Raises
94
+ ------
95
+ VertexNotFoundError
96
+ If either vertex does not exist.
97
+ EdgeNotFoundError
98
+ If the edge does not exist.
99
+ """
100
+ with self._lock:
101
+ if u not in self._adj:
102
+ raise VertexNotFoundError(u)
103
+ if v not in self._adj:
104
+ raise VertexNotFoundError(v)
105
+ if v not in self._adj[u]:
106
+ raise EdgeNotFoundError(u, v)
107
+ del self._adj[u][v]
108
+ self._edge_count -= 1
109
+ if not self._directed and u != v:
110
+ self._adj[v].pop(u, None)
111
+
112
+ def clear(self) -> None:
113
+ """Remove all vertices and edges."""
114
+ with self._lock:
115
+ self._adj.clear()
116
+ self._edge_count = 0
117
+
118
+ # ------------------------------------------------------------------
119
+ # Access
120
+ # ------------------------------------------------------------------
121
+
122
+ def has_vertex(self, v: Any) -> bool:
123
+ """Return *True* if *v* is in the graph."""
124
+ with self._lock:
125
+ return v in self._adj
126
+
127
+ def has_edge(self, u: Any, v: Any) -> bool:
128
+ """Return *True* if the edge (u, v) exists."""
129
+ with self._lock:
130
+ return u in self._adj and v in self._adj[u]
131
+
132
+ def get_weight(self, u: Any, v: Any) -> float:
133
+ """Return the weight of edge (u, v).
134
+
135
+ Raises
136
+ ------
137
+ VertexNotFoundError
138
+ If either vertex does not exist.
139
+ EdgeNotFoundError
140
+ If the edge does not exist.
141
+ """
142
+ with self._lock:
143
+ if u not in self._adj:
144
+ raise VertexNotFoundError(u)
145
+ if v not in self._adj[u]:
146
+ raise EdgeNotFoundError(u, v)
147
+ return self._adj[u][v]
148
+
149
+ def set_weight(self, u: Any, v: Any, weight: float) -> None:
150
+ """Set the weight of edge (u, v).
151
+
152
+ Raises
153
+ ------
154
+ VertexNotFoundError
155
+ If either vertex does not exist.
156
+ EdgeNotFoundError
157
+ If the edge does not exist.
158
+ """
159
+ with self._lock:
160
+ if u not in self._adj:
161
+ raise VertexNotFoundError(u)
162
+ if v not in self._adj[u]:
163
+ raise EdgeNotFoundError(u, v)
164
+ self._adj[u][v] = weight
165
+ if not self._directed and u != v:
166
+ self._adj[v][u] = weight
167
+
168
+ def get_neighbors(self, v: Any) -> list[Any]:
169
+ """Return a list of neighbors of *v* (outgoing in directed mode).
170
+
171
+ Raises
172
+ ------
173
+ VertexNotFoundError
174
+ If *v* is not in the graph.
175
+ """
176
+ with self._lock:
177
+ if v not in self._adj:
178
+ raise VertexNotFoundError(v)
179
+ return list(self._adj[v].keys())
180
+
181
+ def get_vertices(self) -> list[Any]:
182
+ """Return a list of all vertices in the graph."""
183
+ with self._lock:
184
+ return list(self._adj.keys())
185
+
186
+ def get_edges(self) -> list[tuple[Any, Any, float]]:
187
+ """Return a list of all edges as (u, v, weight) tuples."""
188
+ with self._lock:
189
+ edges: list[tuple[Any, Any, float]] = []
190
+ seen: set[frozenset] = set()
191
+ for u in self._adj:
192
+ for v, w in self._adj[u].items():
193
+ if not self._directed:
194
+ key = frozenset((u, v))
195
+ if key in seen:
196
+ continue
197
+ seen.add(key)
198
+ edges.append((u, v, w))
199
+ return edges
200
+
201
+ def degree(self, v: Any) -> int:
202
+ """Return the degree of vertex *v*.
203
+
204
+ For directed graphs, returns the out-degree (number of outgoing edges).
205
+
206
+ Raises
207
+ ------
208
+ VertexNotFoundError
209
+ If *v* is not in the graph.
210
+ """
211
+ with self._lock:
212
+ if v not in self._adj:
213
+ raise VertexNotFoundError(v)
214
+ return len(self._adj[v])
215
+
216
+ def order(self) -> int:
217
+ """Return the number of vertices in the graph."""
218
+ with self._lock:
219
+ return len(self._adj)
220
+
221
+ def edge_count(self) -> int:
222
+ """Return the number of edges in the graph."""
223
+ with self._lock:
224
+ return self._edge_count
225
+
226
+ def is_directed(self) -> bool:
227
+ """Return *True* if this is a directed graph."""
228
+ return self._directed
229
+
230
+ def is_empty(self) -> bool:
231
+ """Return *True* if the graph has no vertices."""
232
+ with self._lock:
233
+ return len(self._adj) == 0
234
+
235
+ def copy(self) -> Graph:
236
+ """Return a deep copy of the graph."""
237
+ with self._lock:
238
+ new_graph = Graph(directed=self._directed)
239
+ for v in self._adj:
240
+ new_graph._adj[v] = dict(self._adj[v])
241
+ new_graph._edge_count = self._edge_count
242
+ return new_graph
243
+
244
+ # ------------------------------------------------------------------
245
+ # Dunders
246
+ # ------------------------------------------------------------------
247
+
248
+ def __len__(self) -> int:
249
+ return self.order()
250
+
251
+ def __contains__(self, v: Any) -> bool:
252
+ return self.has_vertex(v)
253
+
254
+ def __iter__(self) -> Iterator[Any]:
255
+ vertices = self.get_vertices()
256
+ return iter(vertices)
257
+
258
+ def __repr__(self) -> str:
259
+ with self._lock:
260
+ vertices = list(self._adj.keys())
261
+ edges = self.get_edges()
262
+ return f"Graph(directed={self._directed}, vertices={len(vertices)}, edges={len(edges)})"
pkstruct/graphs/mst.py ADDED
@@ -0,0 +1,118 @@
1
+ """
2
+ pkstruct.graphs.mst
3
+ ===================
4
+ Minimum spanning tree algorithms: Kruskal and Prim.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import heapq
10
+ from typing import Any
11
+
12
+ from pkstruct.graphs.graph import Graph
13
+ from pkstruct.graphs.exceptions import InvalidGraphOperationError
14
+
15
+
16
+ def kruskal(graph: Graph) -> list[tuple[Any, Any, float]]:
17
+ """Compute the Minimum Spanning Tree using Kruskal's algorithm.
18
+
19
+ Parameters
20
+ ----------
21
+ graph : Graph
22
+ An undirected weighted graph.
23
+
24
+ Returns
25
+ -------
26
+ list[tuple[Any, Any, float]]
27
+ Edges of the MST as ``(u, v, weight)`` tuples.
28
+
29
+ Raises
30
+ ------
31
+ InvalidGraphOperationError
32
+ If the graph is directed.
33
+ """
34
+ if graph.is_directed():
35
+ raise InvalidGraphOperationError("Kruskal's algorithm requires an undirected graph.")
36
+
37
+ edges = sorted(graph.get_edges(), key=lambda e: e[2])
38
+ parent: dict[Any, Any] = {}
39
+ rank: dict[Any, int] = {}
40
+
41
+ def find(x: Any) -> Any:
42
+ while parent[x] != x:
43
+ parent[x] = parent[parent[x]]
44
+ x = parent[x]
45
+ return x
46
+
47
+ def union(x: Any, y: Any) -> None:
48
+ rx, ry = find(x), find(y)
49
+ if rx == ry:
50
+ return
51
+ if rank[rx] < rank[ry]:
52
+ parent[rx] = ry
53
+ elif rank[rx] > rank[ry]:
54
+ parent[ry] = rx
55
+ else:
56
+ parent[ry] = rx
57
+ rank[rx] += 1
58
+
59
+ for v in graph:
60
+ parent[v] = v
61
+ rank[v] = 0
62
+
63
+ mst: list[tuple[Any, Any, float]] = []
64
+ for u, v, w in edges:
65
+ if find(u) != find(v):
66
+ union(u, v)
67
+ mst.append((u, v, w))
68
+
69
+ return mst
70
+
71
+
72
+ def prim(graph: Graph) -> list[tuple[Any, Any, float]]:
73
+ """Compute the Minimum Spanning Tree using Prim's algorithm.
74
+
75
+ Parameters
76
+ ----------
77
+ graph : Graph
78
+ An undirected weighted graph.
79
+
80
+ Returns
81
+ -------
82
+ list[tuple[Any, Any, float]]
83
+ Edges of the MST as ``(u, v, weight)`` tuples.
84
+
85
+ Raises
86
+ ------
87
+ InvalidGraphOperationError
88
+ If the graph is directed or empty.
89
+ """
90
+ if graph.is_directed():
91
+ raise InvalidGraphOperationError("Prim's algorithm requires an undirected graph.")
92
+
93
+ vertices = list(graph)
94
+ if not vertices:
95
+ raise InvalidGraphOperationError("Graph is empty.")
96
+
97
+ start = vertices[0]
98
+ visited: set[Any] = {start}
99
+ pq: list[tuple[float, Any, Any]] = []
100
+
101
+ for neighbor in graph.get_neighbors(start):
102
+ weight = graph.get_weight(start, neighbor)
103
+ heapq.heappush(pq, (weight, start, neighbor))
104
+
105
+ mst: list[tuple[Any, Any, float]] = []
106
+
107
+ while pq and len(visited) < len(vertices):
108
+ w, u, v = heapq.heappop(pq)
109
+ if v in visited:
110
+ continue
111
+ visited.add(v)
112
+ mst.append((u, v, w))
113
+ for neighbor in graph.get_neighbors(v):
114
+ if neighbor not in visited:
115
+ weight = graph.get_weight(v, neighbor)
116
+ heapq.heappush(pq, (weight, v, neighbor))
117
+
118
+ return mst
pkstruct/graphs/scc.py ADDED
@@ -0,0 +1,138 @@
1
+ """
2
+ pkstruct.graphs.scc
3
+ ===================
4
+ Strongly connected components algorithms: Kosaraju and Tarjan.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from pkstruct.graphs.graph import Graph
12
+ from pkstruct.graphs.directed import DirectedGraph
13
+ from pkstruct.graphs.exceptions import InvalidGraphOperationError
14
+
15
+
16
+ def kosaraju(graph: Graph) -> list[list[Any]]:
17
+ """Find strongly connected components using Kosaraju's algorithm.
18
+
19
+ Parameters
20
+ ----------
21
+ graph : Graph
22
+ A directed graph.
23
+
24
+ Returns
25
+ -------
26
+ list[list[Any]]
27
+ List of SCCs, each being a list of vertices.
28
+
29
+ Raises
30
+ ------
31
+ InvalidGraphOperationError
32
+ If the graph is undirected.
33
+ """
34
+ if not graph.is_directed():
35
+ raise InvalidGraphOperationError("Kosaraju's algorithm requires a directed graph.")
36
+
37
+ visited: set[Any] = set()
38
+ finish_stack: list[Any] = []
39
+
40
+ def _dfs(v: Any) -> None:
41
+ visited.add(v)
42
+ for neighbor in graph.get_neighbors(v):
43
+ if neighbor not in visited:
44
+ _dfs(neighbor)
45
+ finish_stack.append(v)
46
+
47
+ for v in graph:
48
+ if v not in visited:
49
+ _dfs(v)
50
+
51
+ if isinstance(graph, DirectedGraph):
52
+ transposed = graph.reverse()
53
+ else:
54
+ transposed = DirectedGraph()
55
+ for v in graph:
56
+ transposed.add_vertex(v)
57
+ for u in graph:
58
+ for v, w in {n: graph.get_weight(u, n) for n in graph.get_neighbors(u)}.items():
59
+ transposed.add_edge(v, u, w)
60
+
61
+ visited.clear()
62
+ sccs: list[list[Any]] = []
63
+
64
+ def _dfs_reverse(v: Any, component: list[Any]) -> None:
65
+ visited.add(v)
66
+ component.append(v)
67
+ for neighbor in transposed.get_neighbors(v):
68
+ if neighbor not in visited:
69
+ _dfs_reverse(neighbor, component)
70
+
71
+ while finish_stack:
72
+ v = finish_stack.pop()
73
+ if v not in visited:
74
+ component: list[Any] = []
75
+ _dfs_reverse(v, component)
76
+ sccs.append(component)
77
+
78
+ return sccs
79
+
80
+
81
+ def tarjan(graph: Graph) -> list[list[Any]]:
82
+ """Find strongly connected components using Tarjan's algorithm.
83
+
84
+ Parameters
85
+ ----------
86
+ graph : Graph
87
+ A directed graph.
88
+
89
+ Returns
90
+ -------
91
+ list[list[Any]]
92
+ List of SCCs, each being a list of vertices.
93
+
94
+ Raises
95
+ ------
96
+ InvalidGraphOperationError
97
+ If the graph is undirected.
98
+ """
99
+ if not graph.is_directed():
100
+ raise InvalidGraphOperationError("Tarjan's algorithm requires a directed graph.")
101
+
102
+ index_counter: int = 0
103
+ index: dict[Any, int] = {}
104
+ lowlink: dict[Any, int] = {}
105
+ stack: list[Any] = []
106
+ on_stack: set[Any] = set()
107
+ sccs: list[list[Any]] = []
108
+
109
+ def _strongconnect(v: Any) -> None:
110
+ nonlocal index_counter
111
+ index[v] = index_counter
112
+ lowlink[v] = index_counter
113
+ index_counter += 1
114
+ stack.append(v)
115
+ on_stack.add(v)
116
+
117
+ for neighbor in graph.get_neighbors(v):
118
+ if neighbor not in index:
119
+ _strongconnect(neighbor)
120
+ lowlink[v] = min(lowlink[v], lowlink[neighbor])
121
+ elif neighbor in on_stack:
122
+ lowlink[v] = min(lowlink[v], index[neighbor])
123
+
124
+ if lowlink[v] == index[v]:
125
+ component: list[Any] = []
126
+ while True:
127
+ w = stack.pop()
128
+ on_stack.discard(w)
129
+ component.append(w)
130
+ if w == v:
131
+ break
132
+ sccs.append(component)
133
+
134
+ for v in graph:
135
+ if v not in index:
136
+ _strongconnect(v)
137
+
138
+ return sccs