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
pkstruct/__init__.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pkstruct - Production-grade modular data structures toolkit for Python.
|
|
3
|
+
|
|
4
|
+
This package provides industrial-strength implementations of fundamental
|
|
5
|
+
data structures with thread safety, serialization, visualization, and
|
|
6
|
+
comprehensive algorithmic helpers.
|
|
7
|
+
|
|
8
|
+
Modules
|
|
9
|
+
-------
|
|
10
|
+
graphs
|
|
11
|
+
Graph, DirectedGraph, WeightedGraph, traversal, shortest path, MST, SCC
|
|
12
|
+
trees
|
|
13
|
+
BinarySearchTree, AVLTree, RedBlackTree, BTree, BPlusTree,
|
|
14
|
+
SegmentTree, FenwickTree, IntervalTree
|
|
15
|
+
linear
|
|
16
|
+
SinglyLinkedList, DoublyLinkedList, CircularLinkedList
|
|
17
|
+
shared
|
|
18
|
+
Infrastructure components (exceptions, validators, serializers, etc.)
|
|
19
|
+
|
|
20
|
+
Example
|
|
21
|
+
-------
|
|
22
|
+
>>> from pkstruct import BinarySearchTree
|
|
23
|
+
>>> bst = BinarySearchTree()
|
|
24
|
+
>>> bst.insert(10)
|
|
25
|
+
>>> bst.insert(5)
|
|
26
|
+
>>> bst.insert(15)
|
|
27
|
+
>>> list(bst)
|
|
28
|
+
[5, 10, 15]
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from pkstruct.graphs import (
|
|
32
|
+
Graph,
|
|
33
|
+
DirectedGraph,
|
|
34
|
+
WeightedGraph,
|
|
35
|
+
bfs,
|
|
36
|
+
dfs,
|
|
37
|
+
dijkstra,
|
|
38
|
+
bellman_ford,
|
|
39
|
+
floyd_warshall,
|
|
40
|
+
kruskal,
|
|
41
|
+
prim,
|
|
42
|
+
connected_components,
|
|
43
|
+
is_bipartite,
|
|
44
|
+
has_cycle,
|
|
45
|
+
topological_sort_kahn,
|
|
46
|
+
topological_sort_dfs,
|
|
47
|
+
kosaraju,
|
|
48
|
+
tarjan,
|
|
49
|
+
visualize,
|
|
50
|
+
GraphError,
|
|
51
|
+
VertexNotFoundError,
|
|
52
|
+
EdgeNotFoundError,
|
|
53
|
+
InvalidGraphOperationError,
|
|
54
|
+
NegativeCycleError,
|
|
55
|
+
NoPathError,
|
|
56
|
+
)
|
|
57
|
+
from pkstruct.linear import (
|
|
58
|
+
ArrayStack,
|
|
59
|
+
CircularLinkedList,
|
|
60
|
+
CircularQueue,
|
|
61
|
+
ConcurrencyError,
|
|
62
|
+
DoublyLinkedList,
|
|
63
|
+
EmptyStructureError,
|
|
64
|
+
IndexOutOfRangeError,
|
|
65
|
+
InvalidRangeError,
|
|
66
|
+
LinkedDeque,
|
|
67
|
+
LinkedQueue,
|
|
68
|
+
LinkedStack,
|
|
69
|
+
PkstructError,
|
|
70
|
+
PriorityQueue,
|
|
71
|
+
QueueFullError,
|
|
72
|
+
SerializationError,
|
|
73
|
+
SinglyLinkedList,
|
|
74
|
+
ValidationError,
|
|
75
|
+
ValueNotFoundError,
|
|
76
|
+
)
|
|
77
|
+
from pkstruct.trees import (
|
|
78
|
+
AVLTree,
|
|
79
|
+
BinarySearchTree,
|
|
80
|
+
BPlusTree,
|
|
81
|
+
BTree,
|
|
82
|
+
DuplicateKeyError,
|
|
83
|
+
EmptyTreeError,
|
|
84
|
+
FenwickTree,
|
|
85
|
+
IndexOutOfBoundsError,
|
|
86
|
+
IntervalTree,
|
|
87
|
+
InvalidIntervalError,
|
|
88
|
+
InvalidOperationError,
|
|
89
|
+
InvalidOrderError,
|
|
90
|
+
KeyNotFoundError,
|
|
91
|
+
RedBlackTree,
|
|
92
|
+
SegmentTree,
|
|
93
|
+
TreeBalanceError,
|
|
94
|
+
TreeError,
|
|
95
|
+
)
|
|
96
|
+
from pkstruct.trees import (
|
|
97
|
+
SerializationError as TreeSerializationError,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
__all__ = [
|
|
101
|
+
# Graphs
|
|
102
|
+
"Graph",
|
|
103
|
+
"DirectedGraph",
|
|
104
|
+
"WeightedGraph",
|
|
105
|
+
"bfs",
|
|
106
|
+
"dfs",
|
|
107
|
+
"dijkstra",
|
|
108
|
+
"bellman_ford",
|
|
109
|
+
"floyd_warshall",
|
|
110
|
+
"kruskal",
|
|
111
|
+
"prim",
|
|
112
|
+
"connected_components",
|
|
113
|
+
"is_bipartite",
|
|
114
|
+
"has_cycle",
|
|
115
|
+
"topological_sort_kahn",
|
|
116
|
+
"topological_sort_dfs",
|
|
117
|
+
"kosaraju",
|
|
118
|
+
"tarjan",
|
|
119
|
+
"visualize",
|
|
120
|
+
"GraphError",
|
|
121
|
+
"VertexNotFoundError",
|
|
122
|
+
"EdgeNotFoundError",
|
|
123
|
+
"InvalidGraphOperationError",
|
|
124
|
+
"NegativeCycleError",
|
|
125
|
+
"NoPathError",
|
|
126
|
+
# Linear
|
|
127
|
+
"SinglyLinkedList",
|
|
128
|
+
"DoublyLinkedList",
|
|
129
|
+
"CircularLinkedList",
|
|
130
|
+
"ArrayStack",
|
|
131
|
+
"LinkedStack",
|
|
132
|
+
"LinkedQueue",
|
|
133
|
+
"CircularQueue",
|
|
134
|
+
"PriorityQueue",
|
|
135
|
+
"LinkedDeque",
|
|
136
|
+
"QueueFullError",
|
|
137
|
+
"PkstructError",
|
|
138
|
+
"ValidationError",
|
|
139
|
+
"IndexOutOfRangeError",
|
|
140
|
+
"ValueNotFoundError",
|
|
141
|
+
"EmptyStructureError",
|
|
142
|
+
"SerializationError",
|
|
143
|
+
"ConcurrencyError",
|
|
144
|
+
"InvalidRangeError",
|
|
145
|
+
# Trees
|
|
146
|
+
"BinarySearchTree",
|
|
147
|
+
"AVLTree",
|
|
148
|
+
"RedBlackTree",
|
|
149
|
+
"BTree",
|
|
150
|
+
"BPlusTree",
|
|
151
|
+
"SegmentTree",
|
|
152
|
+
"FenwickTree",
|
|
153
|
+
"IntervalTree",
|
|
154
|
+
"TreeError",
|
|
155
|
+
"KeyNotFoundError",
|
|
156
|
+
"DuplicateKeyError",
|
|
157
|
+
"EmptyTreeError",
|
|
158
|
+
"InvalidOrderError",
|
|
159
|
+
"InvalidOperationError",
|
|
160
|
+
"TreeBalanceError",
|
|
161
|
+
"TreeSerializationError",
|
|
162
|
+
"InvalidIntervalError",
|
|
163
|
+
"IndexOutOfBoundsError",
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
__version__ = "0.1.0"
|
|
167
|
+
__author__ = "pkstruct contributors"
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pkstruct.graphs
|
|
3
|
+
===============
|
|
4
|
+
|
|
5
|
+
Graph data structures module for the pkstruct ecosystem.
|
|
6
|
+
|
|
7
|
+
Provides production-grade implementations of graph representations and
|
|
8
|
+
classic graph algorithms with thread safety and ASCII visualization.
|
|
9
|
+
|
|
10
|
+
Classes
|
|
11
|
+
-------
|
|
12
|
+
Graph
|
|
13
|
+
Adjacency-list based graph supporting directed/undirected and weighted modes.
|
|
14
|
+
DirectedGraph
|
|
15
|
+
Directed graph with in-degree, out-degree, reverse, sources, and sinks.
|
|
16
|
+
WeightedGraph
|
|
17
|
+
Convenience subclass for weighted undirected graphs.
|
|
18
|
+
|
|
19
|
+
Algorithms
|
|
20
|
+
----------
|
|
21
|
+
Traversal:
|
|
22
|
+
bfs, dfs, bfs_paths, dfs_paths
|
|
23
|
+
Shortest Path:
|
|
24
|
+
dijkstra, bellman_ford, floyd_warshall
|
|
25
|
+
Minimum Spanning Tree:
|
|
26
|
+
kruskal, prim
|
|
27
|
+
Connectivity:
|
|
28
|
+
connected_components, is_connected, is_bipartite, has_cycle, has_cycle_directed
|
|
29
|
+
Topological Sort:
|
|
30
|
+
topological_sort_kahn, topological_sort_dfs
|
|
31
|
+
Strongly Connected Components:
|
|
32
|
+
kosaraju, tarjan
|
|
33
|
+
|
|
34
|
+
Exceptions
|
|
35
|
+
----------
|
|
36
|
+
GraphError, VertexNotFoundError, EdgeNotFoundError, InvalidGraphOperationError,
|
|
37
|
+
NegativeCycleError, NoPathError
|
|
38
|
+
|
|
39
|
+
Example
|
|
40
|
+
-------
|
|
41
|
+
>>> from pkstruct.graphs import Graph
|
|
42
|
+
>>> g = Graph()
|
|
43
|
+
>>> g.add_edge("A", "B")
|
|
44
|
+
>>> g.add_edge("B", "C")
|
|
45
|
+
>>> g.add_edge("A", "C")
|
|
46
|
+
>>> list(g.get_neighbors("A"))
|
|
47
|
+
['B', 'C']
|
|
48
|
+
>>> len(g)
|
|
49
|
+
3
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
from pkstruct.graphs.connectivity import (
|
|
53
|
+
connected_components,
|
|
54
|
+
has_cycle,
|
|
55
|
+
has_cycle_directed,
|
|
56
|
+
is_bipartite,
|
|
57
|
+
is_connected,
|
|
58
|
+
)
|
|
59
|
+
from pkstruct.graphs.directed import DirectedGraph
|
|
60
|
+
from pkstruct.graphs.exceptions import (
|
|
61
|
+
EdgeNotFoundError,
|
|
62
|
+
GraphError,
|
|
63
|
+
InvalidGraphOperationError,
|
|
64
|
+
NegativeCycleError,
|
|
65
|
+
NoPathError,
|
|
66
|
+
VertexNotFoundError,
|
|
67
|
+
)
|
|
68
|
+
from pkstruct.graphs.graph import Graph
|
|
69
|
+
from pkstruct.graphs.mst import kruskal, prim
|
|
70
|
+
from pkstruct.graphs.scc import kosaraju, tarjan
|
|
71
|
+
from pkstruct.graphs.shortest_path import (
|
|
72
|
+
bellman_ford,
|
|
73
|
+
dijkstra,
|
|
74
|
+
floyd_warshall,
|
|
75
|
+
reconstruct_path,
|
|
76
|
+
reconstruct_path_fw,
|
|
77
|
+
)
|
|
78
|
+
from pkstruct.graphs.topo_sort import topological_sort_dfs, topological_sort_kahn
|
|
79
|
+
from pkstruct.graphs.traversal import bfs, bfs_paths, dfs, dfs_paths
|
|
80
|
+
from pkstruct.graphs.visualization import adjacency_matrix, visualize
|
|
81
|
+
from pkstruct.graphs.weighted import WeightedGraph
|
|
82
|
+
|
|
83
|
+
__all__ = [
|
|
84
|
+
# Graph classes
|
|
85
|
+
"Graph",
|
|
86
|
+
"DirectedGraph",
|
|
87
|
+
"WeightedGraph",
|
|
88
|
+
# Traversal
|
|
89
|
+
"bfs",
|
|
90
|
+
"dfs",
|
|
91
|
+
"bfs_paths",
|
|
92
|
+
"dfs_paths",
|
|
93
|
+
# Shortest Path
|
|
94
|
+
"dijkstra",
|
|
95
|
+
"bellman_ford",
|
|
96
|
+
"floyd_warshall",
|
|
97
|
+
"reconstruct_path",
|
|
98
|
+
"reconstruct_path_fw",
|
|
99
|
+
# MST
|
|
100
|
+
"kruskal",
|
|
101
|
+
"prim",
|
|
102
|
+
# Connectivity
|
|
103
|
+
"connected_components",
|
|
104
|
+
"is_connected",
|
|
105
|
+
"is_bipartite",
|
|
106
|
+
"has_cycle",
|
|
107
|
+
"has_cycle_directed",
|
|
108
|
+
# Topological Sort
|
|
109
|
+
"topological_sort_kahn",
|
|
110
|
+
"topological_sort_dfs",
|
|
111
|
+
# SCC
|
|
112
|
+
"kosaraju",
|
|
113
|
+
"tarjan",
|
|
114
|
+
# Visualization
|
|
115
|
+
"visualize",
|
|
116
|
+
"adjacency_matrix",
|
|
117
|
+
# Exceptions
|
|
118
|
+
"GraphError",
|
|
119
|
+
"VertexNotFoundError",
|
|
120
|
+
"EdgeNotFoundError",
|
|
121
|
+
"InvalidGraphOperationError",
|
|
122
|
+
"NegativeCycleError",
|
|
123
|
+
"NoPathError",
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
__version__ = "0.1.0"
|
|
127
|
+
__author__ = "pkstruct contributors"
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pkstruct.graphs.connectivity
|
|
3
|
+
============================
|
|
4
|
+
Graph connectivity algorithms: connected components, bipartite check.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections import deque
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from pkstruct.graphs.graph import Graph
|
|
13
|
+
from pkstruct.graphs.exceptions import VertexNotFoundError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def connected_components(graph: Graph) -> list[list[Any]]:
|
|
17
|
+
"""Return all connected components of an undirected graph.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
graph : Graph
|
|
22
|
+
An undirected graph.
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
list[list[Any]]
|
|
27
|
+
List of components, each being a list of vertices.
|
|
28
|
+
"""
|
|
29
|
+
visited: set[Any] = set()
|
|
30
|
+
components: list[list[Any]] = []
|
|
31
|
+
|
|
32
|
+
for v in graph:
|
|
33
|
+
if v not in visited:
|
|
34
|
+
component: list[Any] = []
|
|
35
|
+
queue: deque[Any] = deque([v])
|
|
36
|
+
visited.add(v)
|
|
37
|
+
while queue:
|
|
38
|
+
current = queue.popleft()
|
|
39
|
+
component.append(current)
|
|
40
|
+
for neighbor in graph.get_neighbors(current):
|
|
41
|
+
if neighbor not in visited:
|
|
42
|
+
visited.add(neighbor)
|
|
43
|
+
queue.append(neighbor)
|
|
44
|
+
components.append(component)
|
|
45
|
+
|
|
46
|
+
return components
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def is_connected(graph: Graph) -> bool:
|
|
50
|
+
"""Return *True* if the undirected graph is connected.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
graph : Graph
|
|
55
|
+
An undirected graph.
|
|
56
|
+
|
|
57
|
+
Returns
|
|
58
|
+
-------
|
|
59
|
+
bool
|
|
60
|
+
"""
|
|
61
|
+
if graph.is_empty():
|
|
62
|
+
return True
|
|
63
|
+
components = connected_components(graph)
|
|
64
|
+
return len(components) == 1
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def is_bipartite(graph: Graph) -> bool:
|
|
68
|
+
"""Check if a graph is bipartite using BFS coloring.
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
graph : Graph
|
|
73
|
+
An undirected graph.
|
|
74
|
+
|
|
75
|
+
Returns
|
|
76
|
+
-------
|
|
77
|
+
bool
|
|
78
|
+
*True* if the graph is bipartite.
|
|
79
|
+
"""
|
|
80
|
+
color: dict[Any, int] = {}
|
|
81
|
+
|
|
82
|
+
for start in graph:
|
|
83
|
+
if start not in color:
|
|
84
|
+
color[start] = 0
|
|
85
|
+
queue: deque[Any] = deque([start])
|
|
86
|
+
while queue:
|
|
87
|
+
v = queue.popleft()
|
|
88
|
+
for neighbor in graph.get_neighbors(v):
|
|
89
|
+
if neighbor not in color:
|
|
90
|
+
color[neighbor] = 1 - color[v]
|
|
91
|
+
queue.append(neighbor)
|
|
92
|
+
elif color[neighbor] == color[v]:
|
|
93
|
+
return False
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def has_cycle(graph: Graph) -> bool:
|
|
98
|
+
"""Detect if an undirected graph contains a cycle.
|
|
99
|
+
|
|
100
|
+
Parameters
|
|
101
|
+
----------
|
|
102
|
+
graph : Graph
|
|
103
|
+
An undirected graph.
|
|
104
|
+
|
|
105
|
+
Returns
|
|
106
|
+
-------
|
|
107
|
+
bool
|
|
108
|
+
"""
|
|
109
|
+
visited: set[Any] = set()
|
|
110
|
+
|
|
111
|
+
def _dfs(v: Any, parent: Any | None) -> bool:
|
|
112
|
+
visited.add(v)
|
|
113
|
+
for neighbor in graph.get_neighbors(v):
|
|
114
|
+
if neighbor not in visited:
|
|
115
|
+
if _dfs(neighbor, v):
|
|
116
|
+
return True
|
|
117
|
+
elif neighbor != parent:
|
|
118
|
+
return True
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
for v in graph:
|
|
122
|
+
if v not in visited:
|
|
123
|
+
if _dfs(v, None):
|
|
124
|
+
return True
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def has_cycle_directed(graph: Graph) -> bool:
|
|
129
|
+
"""Detect if a directed graph contains a cycle using DFS coloring.
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
graph : Graph
|
|
134
|
+
A directed graph.
|
|
135
|
+
|
|
136
|
+
Returns
|
|
137
|
+
-------
|
|
138
|
+
bool
|
|
139
|
+
"""
|
|
140
|
+
WHITE, GRAY, BLACK = 0, 1, 2
|
|
141
|
+
color: dict[Any, int] = {v: WHITE for v in graph}
|
|
142
|
+
|
|
143
|
+
def _dfs(v: Any) -> bool:
|
|
144
|
+
color[v] = GRAY
|
|
145
|
+
for neighbor in graph.get_neighbors(v):
|
|
146
|
+
if color[neighbor] == GRAY:
|
|
147
|
+
return True
|
|
148
|
+
if color[neighbor] == WHITE and _dfs(neighbor):
|
|
149
|
+
return True
|
|
150
|
+
color[v] = BLACK
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
for v in graph:
|
|
154
|
+
if color[v] == WHITE:
|
|
155
|
+
if _dfs(v):
|
|
156
|
+
return True
|
|
157
|
+
return False
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pkstruct.graphs.directed
|
|
3
|
+
========================
|
|
4
|
+
Directed graph implementation with direction-specific operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Iterator
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from pkstruct.graphs.exceptions import VertexNotFoundError
|
|
13
|
+
from pkstruct.graphs.graph import Graph
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DirectedGraph(Graph):
|
|
17
|
+
"""A directed graph (convenience subclass of ``Graph`` with ``directed=True``).
|
|
18
|
+
|
|
19
|
+
Provides additional methods specific to directed graphs such as
|
|
20
|
+
in-degree, out-degree, transpose, sources, and sinks.
|
|
21
|
+
|
|
22
|
+
Example
|
|
23
|
+
-------
|
|
24
|
+
>>> g = DirectedGraph()
|
|
25
|
+
>>> g.add_edge("A", "B")
|
|
26
|
+
>>> g.add_edge("B", "C")
|
|
27
|
+
>>> g.add_edge("A", "C")
|
|
28
|
+
>>> g.out_degree("A")
|
|
29
|
+
2
|
|
30
|
+
>>> g.in_degree("C")
|
|
31
|
+
2
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self) -> None:
|
|
35
|
+
super().__init__(directed=True)
|
|
36
|
+
|
|
37
|
+
def in_degree(self, v: Any) -> int:
|
|
38
|
+
"""Return the number of incoming edges to *v*.
|
|
39
|
+
|
|
40
|
+
Raises
|
|
41
|
+
------
|
|
42
|
+
VertexNotFoundError
|
|
43
|
+
If *v* is not in the graph.
|
|
44
|
+
"""
|
|
45
|
+
with self._lock:
|
|
46
|
+
if v not in self._adj:
|
|
47
|
+
raise VertexNotFoundError(v)
|
|
48
|
+
count = 0
|
|
49
|
+
for u in self._adj:
|
|
50
|
+
if v in self._adj[u]:
|
|
51
|
+
count += 1
|
|
52
|
+
return count
|
|
53
|
+
|
|
54
|
+
def out_degree(self, v: Any) -> int:
|
|
55
|
+
"""Return the number of outgoing edges from *v*.
|
|
56
|
+
|
|
57
|
+
Raises
|
|
58
|
+
------
|
|
59
|
+
VertexNotFoundError
|
|
60
|
+
If *v* is not in the graph.
|
|
61
|
+
"""
|
|
62
|
+
return self.degree(v)
|
|
63
|
+
|
|
64
|
+
def sources(self) -> list[Any]:
|
|
65
|
+
"""Return all vertices with in-degree 0."""
|
|
66
|
+
with self._lock:
|
|
67
|
+
return [v for v in self._adj if self.in_degree(v) == 0]
|
|
68
|
+
|
|
69
|
+
def sinks(self) -> list[Any]:
|
|
70
|
+
"""Return all vertices with out-degree 0."""
|
|
71
|
+
with self._lock:
|
|
72
|
+
return [v for v in self._adj if self.out_degree(v) == 0]
|
|
73
|
+
|
|
74
|
+
def reverse(self) -> DirectedGraph:
|
|
75
|
+
"""Return the transpose (reverse all edges).
|
|
76
|
+
|
|
77
|
+
Returns
|
|
78
|
+
-------
|
|
79
|
+
DirectedGraph
|
|
80
|
+
A new graph with every edge (u, v) replaced by (v, u).
|
|
81
|
+
"""
|
|
82
|
+
with self._lock:
|
|
83
|
+
rev = DirectedGraph()
|
|
84
|
+
for u in self._adj:
|
|
85
|
+
rev.add_vertex(u)
|
|
86
|
+
for u in self._adj:
|
|
87
|
+
for v, w in self._adj[u].items():
|
|
88
|
+
rev.add_edge(v, u, w)
|
|
89
|
+
return rev
|
|
90
|
+
|
|
91
|
+
def __repr__(self) -> str:
|
|
92
|
+
with self._lock:
|
|
93
|
+
vertices = list(self._adj.keys())
|
|
94
|
+
edges = self.get_edges()
|
|
95
|
+
return f"DirectedGraph(vertices={len(vertices)}, edges={len(edges)})"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pkstruct.graphs.exceptions
|
|
3
|
+
==========================
|
|
4
|
+
Custom exception hierarchy for the graphs module.
|
|
5
|
+
|
|
6
|
+
All graph-specific exceptions inherit from :class:`GraphError` so callers
|
|
7
|
+
can catch the entire family with a single ``except GraphError`` clause.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"GraphError",
|
|
14
|
+
"VertexNotFoundError",
|
|
15
|
+
"EdgeNotFoundError",
|
|
16
|
+
"InvalidGraphOperationError",
|
|
17
|
+
"NegativeCycleError",
|
|
18
|
+
"NoPathError",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class GraphError(Exception):
|
|
23
|
+
"""Base exception for all graph-related errors."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class VertexNotFoundError(GraphError):
|
|
27
|
+
"""Raised when a vertex does not exist in the graph."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, vertex: object) -> None:
|
|
30
|
+
super().__init__(f"Vertex not found: {vertex!r}")
|
|
31
|
+
self.vertex = vertex
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class EdgeNotFoundError(GraphError):
|
|
35
|
+
"""Raised when an edge does not exist in the graph."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, u: object, v: object) -> None:
|
|
38
|
+
super().__init__(f"Edge not found: ({u!r}, {v!r})")
|
|
39
|
+
self.u = u
|
|
40
|
+
self.v = v
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class InvalidGraphOperationError(GraphError):
|
|
44
|
+
"""Raised when an operation is invalid for the current graph state."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, message: str) -> None:
|
|
47
|
+
super().__init__(message)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class NegativeCycleError(GraphError):
|
|
51
|
+
"""Raised when a negative-weight cycle is detected."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, message: str = "Graph contains a negative-weight cycle") -> None:
|
|
54
|
+
super().__init__(message)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class NoPathError(GraphError):
|
|
58
|
+
"""Raised when no path exists between two vertices."""
|
|
59
|
+
|
|
60
|
+
def __init__(self, source: object, target: object) -> None:
|
|
61
|
+
super().__init__(f"No path from {source!r} to {target!r}")
|
|
62
|
+
self.source = source
|
|
63
|
+
self.target = target
|