exonware-xwnode 0.0.1.21__py3-none-any.whl → 0.0.1.23__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.
- exonware/__init__.py +8 -1
- exonware/xwnode/__init__.py +18 -5
- exonware/xwnode/add_strategy_types.py +165 -0
- exonware/xwnode/base.py +7 -5
- exonware/xwnode/common/__init__.py +1 -1
- exonware/xwnode/common/graph/__init__.py +30 -0
- exonware/xwnode/common/graph/caching.py +131 -0
- exonware/xwnode/common/graph/contracts.py +100 -0
- exonware/xwnode/common/graph/errors.py +44 -0
- exonware/xwnode/common/graph/indexing.py +260 -0
- exonware/xwnode/common/graph/manager.py +568 -0
- exonware/xwnode/common/management/__init__.py +3 -5
- exonware/xwnode/common/management/manager.py +9 -9
- exonware/xwnode/common/management/migration.py +6 -6
- exonware/xwnode/common/monitoring/__init__.py +3 -5
- exonware/xwnode/common/monitoring/metrics.py +7 -3
- exonware/xwnode/common/monitoring/pattern_detector.py +2 -2
- exonware/xwnode/common/monitoring/performance_monitor.py +6 -2
- exonware/xwnode/common/patterns/__init__.py +3 -5
- exonware/xwnode/common/patterns/advisor.py +1 -1
- exonware/xwnode/common/patterns/flyweight.py +6 -2
- exonware/xwnode/common/patterns/registry.py +203 -184
- exonware/xwnode/common/utils/__init__.py +25 -11
- exonware/xwnode/common/utils/simple.py +1 -1
- exonware/xwnode/config.py +3 -8
- exonware/xwnode/contracts.py +4 -105
- exonware/xwnode/defs.py +413 -159
- exonware/xwnode/edges/strategies/__init__.py +86 -4
- exonware/xwnode/edges/strategies/_base_edge.py +2 -2
- exonware/xwnode/edges/strategies/adj_list.py +287 -121
- exonware/xwnode/edges/strategies/adj_matrix.py +316 -222
- exonware/xwnode/edges/strategies/base.py +1 -1
- exonware/xwnode/edges/strategies/{edge_bidir_wrapper.py → bidir_wrapper.py} +45 -4
- exonware/xwnode/edges/strategies/bitemporal.py +520 -0
- exonware/xwnode/edges/strategies/{edge_block_adj_matrix.py → block_adj_matrix.py} +77 -6
- exonware/xwnode/edges/strategies/bv_graph.py +664 -0
- exonware/xwnode/edges/strategies/compressed_graph.py +217 -0
- exonware/xwnode/edges/strategies/{edge_coo.py → coo.py} +46 -4
- exonware/xwnode/edges/strategies/{edge_csc.py → csc.py} +45 -4
- exonware/xwnode/edges/strategies/{edge_csr.py → csr.py} +94 -12
- exonware/xwnode/edges/strategies/{edge_dynamic_adj_list.py → dynamic_adj_list.py} +46 -4
- exonware/xwnode/edges/strategies/edge_list.py +168 -0
- exonware/xwnode/edges/strategies/edge_property_store.py +2 -2
- exonware/xwnode/edges/strategies/euler_tour.py +560 -0
- exonware/xwnode/edges/strategies/{edge_flow_network.py → flow_network.py} +2 -2
- exonware/xwnode/edges/strategies/graphblas.py +449 -0
- exonware/xwnode/edges/strategies/hnsw.py +637 -0
- exonware/xwnode/edges/strategies/hop2_labels.py +467 -0
- exonware/xwnode/edges/strategies/{edge_hyperedge_set.py → hyperedge_set.py} +2 -2
- exonware/xwnode/edges/strategies/incidence_matrix.py +250 -0
- exonware/xwnode/edges/strategies/k2_tree.py +613 -0
- exonware/xwnode/edges/strategies/link_cut.py +626 -0
- exonware/xwnode/edges/strategies/multiplex.py +532 -0
- exonware/xwnode/edges/strategies/{edge_neural_graph.py → neural_graph.py} +2 -2
- exonware/xwnode/edges/strategies/{edge_octree.py → octree.py} +69 -11
- exonware/xwnode/edges/strategies/{edge_quadtree.py → quadtree.py} +66 -10
- exonware/xwnode/edges/strategies/roaring_adj.py +438 -0
- exonware/xwnode/edges/strategies/{edge_rtree.py → rtree.py} +43 -5
- exonware/xwnode/edges/strategies/{edge_temporal_edgeset.py → temporal_edgeset.py} +24 -5
- exonware/xwnode/edges/strategies/{edge_tree_graph_basic.py → tree_graph_basic.py} +78 -7
- exonware/xwnode/edges/strategies/{edge_weighted_graph.py → weighted_graph.py} +188 -10
- exonware/xwnode/errors.py +3 -6
- exonware/xwnode/facade.py +20 -20
- exonware/xwnode/nodes/strategies/__init__.py +29 -9
- exonware/xwnode/nodes/strategies/adjacency_list.py +650 -177
- exonware/xwnode/nodes/strategies/aho_corasick.py +358 -183
- exonware/xwnode/nodes/strategies/array_list.py +36 -3
- exonware/xwnode/nodes/strategies/art.py +581 -0
- exonware/xwnode/nodes/strategies/{node_avl_tree.py → avl_tree.py} +77 -6
- exonware/xwnode/nodes/strategies/{node_b_plus_tree.py → b_plus_tree.py} +81 -40
- exonware/xwnode/nodes/strategies/{node_btree.py → b_tree.py} +79 -9
- exonware/xwnode/nodes/strategies/base.py +469 -98
- exonware/xwnode/nodes/strategies/{node_bitmap.py → bitmap.py} +12 -12
- exonware/xwnode/nodes/strategies/{node_bitset_dynamic.py → bitset_dynamic.py} +11 -11
- exonware/xwnode/nodes/strategies/{node_bloom_filter.py → bloom_filter.py} +15 -2
- exonware/xwnode/nodes/strategies/bloomier_filter.py +519 -0
- exonware/xwnode/nodes/strategies/bw_tree.py +531 -0
- exonware/xwnode/nodes/strategies/contracts.py +1 -1
- exonware/xwnode/nodes/strategies/{node_count_min_sketch.py → count_min_sketch.py} +3 -2
- exonware/xwnode/nodes/strategies/{node_cow_tree.py → cow_tree.py} +135 -13
- exonware/xwnode/nodes/strategies/crdt_map.py +629 -0
- exonware/xwnode/nodes/strategies/{node_cuckoo_hash.py → cuckoo_hash.py} +2 -2
- exonware/xwnode/nodes/strategies/{node_xdata_optimized.py → data_interchange_optimized.py} +21 -4
- exonware/xwnode/nodes/strategies/dawg.py +876 -0
- exonware/xwnode/nodes/strategies/deque.py +321 -153
- exonware/xwnode/nodes/strategies/extendible_hash.py +93 -0
- exonware/xwnode/nodes/strategies/{node_fenwick_tree.py → fenwick_tree.py} +111 -19
- exonware/xwnode/nodes/strategies/hamt.py +403 -0
- exonware/xwnode/nodes/strategies/hash_map.py +354 -67
- exonware/xwnode/nodes/strategies/heap.py +105 -5
- exonware/xwnode/nodes/strategies/hopscotch_hash.py +525 -0
- exonware/xwnode/nodes/strategies/{node_hyperloglog.py → hyperloglog.py} +6 -5
- exonware/xwnode/nodes/strategies/interval_tree.py +742 -0
- exonware/xwnode/nodes/strategies/kd_tree.py +703 -0
- exonware/xwnode/nodes/strategies/learned_index.py +533 -0
- exonware/xwnode/nodes/strategies/linear_hash.py +93 -0
- exonware/xwnode/nodes/strategies/linked_list.py +316 -119
- exonware/xwnode/nodes/strategies/{node_lsm_tree.py → lsm_tree.py} +219 -15
- exonware/xwnode/nodes/strategies/masstree.py +130 -0
- exonware/xwnode/nodes/strategies/{node_persistent_tree.py → persistent_tree.py} +149 -9
- exonware/xwnode/nodes/strategies/priority_queue.py +544 -132
- exonware/xwnode/nodes/strategies/queue.py +249 -120
- exonware/xwnode/nodes/strategies/{node_red_black_tree.py → red_black_tree.py} +183 -72
- exonware/xwnode/nodes/strategies/{node_roaring_bitmap.py → roaring_bitmap.py} +19 -6
- exonware/xwnode/nodes/strategies/rope.py +717 -0
- exonware/xwnode/nodes/strategies/{node_segment_tree.py → segment_tree.py} +106 -106
- exonware/xwnode/nodes/strategies/{node_set_hash.py → set_hash.py} +30 -29
- exonware/xwnode/nodes/strategies/{node_skip_list.py → skip_list.py} +74 -6
- exonware/xwnode/nodes/strategies/sparse_matrix.py +427 -131
- exonware/xwnode/nodes/strategies/{node_splay_tree.py → splay_tree.py} +55 -6
- exonware/xwnode/nodes/strategies/stack.py +244 -112
- exonware/xwnode/nodes/strategies/{node_suffix_array.py → suffix_array.py} +5 -1
- exonware/xwnode/nodes/strategies/t_tree.py +94 -0
- exonware/xwnode/nodes/strategies/{node_treap.py → treap.py} +75 -6
- exonware/xwnode/nodes/strategies/{node_tree_graph_hybrid.py → tree_graph_hybrid.py} +46 -5
- exonware/xwnode/nodes/strategies/trie.py +153 -9
- exonware/xwnode/nodes/strategies/union_find.py +111 -5
- exonware/xwnode/nodes/strategies/veb_tree.py +856 -0
- exonware/xwnode/strategies/__init__.py +5 -51
- exonware/xwnode/version.py +3 -3
- {exonware_xwnode-0.0.1.21.dist-info → exonware_xwnode-0.0.1.23.dist-info}/METADATA +23 -3
- exonware_xwnode-0.0.1.23.dist-info/RECORD +130 -0
- exonware/xwnode/edges/strategies/edge_adj_list.py +0 -353
- exonware/xwnode/edges/strategies/edge_adj_matrix.py +0 -445
- exonware/xwnode/nodes/strategies/_base_node.py +0 -307
- exonware/xwnode/nodes/strategies/node_aho_corasick.py +0 -525
- exonware/xwnode/nodes/strategies/node_array_list.py +0 -179
- exonware/xwnode/nodes/strategies/node_hash_map.py +0 -273
- exonware/xwnode/nodes/strategies/node_heap.py +0 -196
- exonware/xwnode/nodes/strategies/node_linked_list.py +0 -413
- exonware/xwnode/nodes/strategies/node_trie.py +0 -257
- exonware/xwnode/nodes/strategies/node_union_find.py +0 -192
- exonware/xwnode/queries/executors/__init__.py +0 -47
- exonware/xwnode/queries/executors/advanced/__init__.py +0 -37
- exonware/xwnode/queries/executors/advanced/aggregate_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/ask_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/construct_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/describe_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/for_loop_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/foreach_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/join_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/let_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/mutation_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/options_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/pipe_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/subscribe_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/subscription_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/union_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/window_executor.py +0 -51
- exonware/xwnode/queries/executors/advanced/with_cte_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/__init__.py +0 -21
- exonware/xwnode/queries/executors/aggregation/avg_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/count_executor.py +0 -38
- exonware/xwnode/queries/executors/aggregation/distinct_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/group_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/having_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/max_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/min_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/sum_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/summarize_executor.py +0 -50
- exonware/xwnode/queries/executors/array/__init__.py +0 -9
- exonware/xwnode/queries/executors/array/indexing_executor.py +0 -51
- exonware/xwnode/queries/executors/array/slicing_executor.py +0 -51
- exonware/xwnode/queries/executors/base.py +0 -257
- exonware/xwnode/queries/executors/capability_checker.py +0 -204
- exonware/xwnode/queries/executors/contracts.py +0 -166
- exonware/xwnode/queries/executors/core/__init__.py +0 -17
- exonware/xwnode/queries/executors/core/create_executor.py +0 -96
- exonware/xwnode/queries/executors/core/delete_executor.py +0 -99
- exonware/xwnode/queries/executors/core/drop_executor.py +0 -100
- exonware/xwnode/queries/executors/core/insert_executor.py +0 -39
- exonware/xwnode/queries/executors/core/select_executor.py +0 -152
- exonware/xwnode/queries/executors/core/update_executor.py +0 -102
- exonware/xwnode/queries/executors/data/__init__.py +0 -13
- exonware/xwnode/queries/executors/data/alter_executor.py +0 -50
- exonware/xwnode/queries/executors/data/load_executor.py +0 -50
- exonware/xwnode/queries/executors/data/merge_executor.py +0 -50
- exonware/xwnode/queries/executors/data/store_executor.py +0 -50
- exonware/xwnode/queries/executors/defs.py +0 -93
- exonware/xwnode/queries/executors/engine.py +0 -221
- exonware/xwnode/queries/executors/errors.py +0 -68
- exonware/xwnode/queries/executors/filtering/__init__.py +0 -25
- exonware/xwnode/queries/executors/filtering/between_executor.py +0 -80
- exonware/xwnode/queries/executors/filtering/filter_executor.py +0 -79
- exonware/xwnode/queries/executors/filtering/has_executor.py +0 -70
- exonware/xwnode/queries/executors/filtering/in_executor.py +0 -70
- exonware/xwnode/queries/executors/filtering/like_executor.py +0 -76
- exonware/xwnode/queries/executors/filtering/optional_executor.py +0 -76
- exonware/xwnode/queries/executors/filtering/range_executor.py +0 -80
- exonware/xwnode/queries/executors/filtering/term_executor.py +0 -77
- exonware/xwnode/queries/executors/filtering/values_executor.py +0 -71
- exonware/xwnode/queries/executors/filtering/where_executor.py +0 -44
- exonware/xwnode/queries/executors/graph/__init__.py +0 -15
- exonware/xwnode/queries/executors/graph/in_traverse_executor.py +0 -51
- exonware/xwnode/queries/executors/graph/match_executor.py +0 -51
- exonware/xwnode/queries/executors/graph/out_executor.py +0 -51
- exonware/xwnode/queries/executors/graph/path_executor.py +0 -51
- exonware/xwnode/queries/executors/graph/return_executor.py +0 -51
- exonware/xwnode/queries/executors/ordering/__init__.py +0 -9
- exonware/xwnode/queries/executors/ordering/by_executor.py +0 -50
- exonware/xwnode/queries/executors/ordering/order_executor.py +0 -51
- exonware/xwnode/queries/executors/projection/__init__.py +0 -9
- exonware/xwnode/queries/executors/projection/extend_executor.py +0 -50
- exonware/xwnode/queries/executors/projection/project_executor.py +0 -50
- exonware/xwnode/queries/executors/registry.py +0 -173
- exonware/xwnode/queries/parsers/__init__.py +0 -26
- exonware/xwnode/queries/parsers/base.py +0 -86
- exonware/xwnode/queries/parsers/contracts.py +0 -46
- exonware/xwnode/queries/parsers/errors.py +0 -53
- exonware/xwnode/queries/parsers/sql_param_extractor.py +0 -318
- exonware/xwnode/queries/strategies/__init__.py +0 -24
- exonware/xwnode/queries/strategies/base.py +0 -236
- exonware/xwnode/queries/strategies/cql.py +0 -201
- exonware/xwnode/queries/strategies/cypher.py +0 -181
- exonware/xwnode/queries/strategies/datalog.py +0 -70
- exonware/xwnode/queries/strategies/elastic_dsl.py +0 -70
- exonware/xwnode/queries/strategies/eql.py +0 -70
- exonware/xwnode/queries/strategies/flux.py +0 -70
- exonware/xwnode/queries/strategies/gql.py +0 -70
- exonware/xwnode/queries/strategies/graphql.py +0 -240
- exonware/xwnode/queries/strategies/gremlin.py +0 -181
- exonware/xwnode/queries/strategies/hiveql.py +0 -214
- exonware/xwnode/queries/strategies/hql.py +0 -70
- exonware/xwnode/queries/strategies/jmespath.py +0 -219
- exonware/xwnode/queries/strategies/jq.py +0 -66
- exonware/xwnode/queries/strategies/json_query.py +0 -66
- exonware/xwnode/queries/strategies/jsoniq.py +0 -248
- exonware/xwnode/queries/strategies/kql.py +0 -70
- exonware/xwnode/queries/strategies/linq.py +0 -238
- exonware/xwnode/queries/strategies/logql.py +0 -70
- exonware/xwnode/queries/strategies/mql.py +0 -68
- exonware/xwnode/queries/strategies/n1ql.py +0 -210
- exonware/xwnode/queries/strategies/partiql.py +0 -70
- exonware/xwnode/queries/strategies/pig.py +0 -215
- exonware/xwnode/queries/strategies/promql.py +0 -70
- exonware/xwnode/queries/strategies/sparql.py +0 -220
- exonware/xwnode/queries/strategies/sql.py +0 -275
- exonware/xwnode/queries/strategies/xml_query.py +0 -66
- exonware/xwnode/queries/strategies/xpath.py +0 -223
- exonware/xwnode/queries/strategies/xquery.py +0 -258
- exonware/xwnode/queries/strategies/xwnode_executor.py +0 -332
- exonware/xwnode/queries/strategies/xwquery.py +0 -456
- exonware_xwnode-0.0.1.21.dist-info/RECORD +0 -214
- /exonware/xwnode/nodes/strategies/{node_ordered_map.py → ordered_map.py} +0 -0
- /exonware/xwnode/nodes/strategies/{node_ordered_map_balanced.py → ordered_map_balanced.py} +0 -0
- /exonware/xwnode/nodes/strategies/{node_patricia.py → patricia.py} +0 -0
- /exonware/xwnode/nodes/strategies/{node_radix_trie.py → radix_trie.py} +0 -0
- /exonware/xwnode/nodes/strategies/{node_set_tree.py → set_tree.py} +0 -0
- {exonware_xwnode-0.0.1.21.dist-info → exonware_xwnode-0.0.1.23.dist-info}/WHEEL +0 -0
- {exonware_xwnode-0.0.1.21.dist-info → exonware_xwnode-0.0.1.23.dist-info}/licenses/LICENSE +0 -0
@@ -1,17 +1,26 @@
|
|
1
1
|
"""
|
2
|
+
#exonware/xwnode/src/exonware/xwnode/nodes/strategies/adjacency_list.py
|
3
|
+
|
2
4
|
Adjacency List Strategy Implementation
|
3
5
|
|
4
|
-
|
6
|
+
Production-grade graph representation using adjacency lists.
|
7
|
+
|
8
|
+
Best Practices Implemented:
|
9
|
+
- Dictionary of lists for O(1) neighbor access
|
10
|
+
- Support for directed and undirected graphs
|
11
|
+
- Weighted edges with efficient storage
|
12
|
+
- Industry-standard graph algorithms (DFS, BFS, topological sort)
|
13
|
+
- Proper graph semantics following CLRS and NetworkX patterns
|
5
14
|
|
6
15
|
Company: eXonware.com
|
7
16
|
Author: Eng. Muhammad AlShehri
|
8
17
|
Email: connect@exonware.com
|
9
|
-
Version: 0.0.1.
|
10
|
-
Generation Date:
|
18
|
+
Version: 0.0.1.23
|
19
|
+
Generation Date: October 12, 2025
|
11
20
|
"""
|
12
21
|
|
13
|
-
from typing import Any, Iterator, List, Optional, Dict,
|
14
|
-
from collections import defaultdict
|
22
|
+
from typing import Any, Iterator, List, Optional, Dict, Set, Tuple, Callable
|
23
|
+
from collections import defaultdict, deque
|
15
24
|
from .base import ANodeGraphStrategy
|
16
25
|
from .contracts import NodeType
|
17
26
|
from ...defs import NodeMode, NodeTrait
|
@@ -19,254 +28,718 @@ from ...defs import NodeMode, NodeTrait
|
|
19
28
|
|
20
29
|
class AdjacencyListStrategy(ANodeGraphStrategy):
|
21
30
|
"""
|
22
|
-
Adjacency List
|
31
|
+
Production-grade Adjacency List graph strategy.
|
32
|
+
|
33
|
+
Optimized for:
|
34
|
+
- Social networks (followers, friends, connections)
|
35
|
+
- Web graphs (links, citations, dependencies)
|
36
|
+
- Routing algorithms (Dijkstra, A*, Bellman-Ford)
|
37
|
+
- Dependency graphs (build systems, package managers)
|
38
|
+
- Recommendation systems (user-item graphs)
|
39
|
+
- Network analysis (PageRank, community detection)
|
40
|
+
|
41
|
+
Format: Dictionary of Lists
|
42
|
+
- Best for: Sparse graphs, dynamic graphs
|
43
|
+
- Storage: node -> list of (neighbor, weight) tuples
|
44
|
+
- Space: O(V + E) where V=vertices, E=edges
|
45
|
+
- Operations: O(degree) neighbor queries
|
46
|
+
|
47
|
+
Performance:
|
48
|
+
- Add vertex: O(1)
|
49
|
+
- Add edge: O(1)
|
50
|
+
- Remove edge: O(degree)
|
51
|
+
- Get neighbors: O(1) to O(degree)
|
52
|
+
- DFS: O(V + E)
|
53
|
+
- BFS: O(V + E)
|
54
|
+
- Topological sort: O(V + E)
|
55
|
+
|
56
|
+
Security:
|
57
|
+
- Vertex existence validation
|
58
|
+
- Cycle detection for safety
|
59
|
+
- Safe edge weight handling
|
23
60
|
|
24
|
-
|
25
|
-
|
61
|
+
Follows eXonware Priorities:
|
62
|
+
1. Security: Input validation, safe traversal
|
63
|
+
2. Usability: Standard graph operations interface
|
64
|
+
3. Maintainability: Clean adjacency list implementation
|
65
|
+
4. Performance: O(V + E) algorithms, O(1) neighbor access
|
66
|
+
5. Extensibility: Easy to add custom graph algorithms
|
67
|
+
"""
|
26
68
|
|
27
69
|
# Strategy type classification
|
28
70
|
STRATEGY_TYPE = NodeType.GRAPH
|
29
|
-
ations in sparse graphs.
|
30
|
-
"""
|
31
71
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
self._mode = NodeMode.ADJACENCY_LIST
|
38
|
-
self._traits = {NodeTrait.GRAPH, NodeTrait.SPARSE, NodeTrait.FAST_NEIGHBORS}
|
39
|
-
|
40
|
-
def insert(self, key: str, value: Any) -> None:
|
41
|
-
"""Insert a node into the graph."""
|
42
|
-
self._nodes[key] = value
|
43
|
-
if key not in self._adj_list:
|
44
|
-
self._adj_list[key] = []
|
45
|
-
|
46
|
-
def find(self, key: str) -> Optional[Any]:
|
47
|
-
"""Find a node in the graph."""
|
48
|
-
return self._nodes.get(key)
|
49
|
-
|
50
|
-
def delete(self, key: str) -> bool:
|
51
|
-
"""Delete a node and all its edges."""
|
52
|
-
if key not in self._nodes:
|
53
|
-
return False
|
72
|
+
__slots__ = ('_adj_list', '_nodes', '_is_directed', '_edge_count')
|
73
|
+
|
74
|
+
def __init__(self, traits: NodeTrait = NodeTrait.NONE, **options):
|
75
|
+
"""
|
76
|
+
Initialize an empty adjacency list.
|
54
77
|
|
55
|
-
|
56
|
-
|
78
|
+
Args:
|
79
|
+
traits: Additional node traits
|
80
|
+
**options:
|
81
|
+
is_directed: True for directed graph, False for undirected
|
82
|
+
initial_vertices: Optional list of initial vertices
|
83
|
+
initial_edges: Optional list of (from, to, weight) tuples
|
84
|
+
"""
|
85
|
+
super().__init__(
|
86
|
+
NodeMode.ADJACENCY_LIST,
|
87
|
+
traits | NodeTrait.GRAPH | NodeTrait.SPARSE | NodeTrait.FAST_NEIGHBORS,
|
88
|
+
**options
|
89
|
+
)
|
57
90
|
|
58
|
-
|
59
|
-
|
60
|
-
|
91
|
+
self._is_directed: bool = options.get('is_directed', True)
|
92
|
+
self._adj_list: Dict[str, List[Tuple[str, float]]] = defaultdict(list)
|
93
|
+
self._nodes: Dict[str, Any] = {}
|
94
|
+
self._edge_count = 0
|
61
95
|
|
62
|
-
#
|
63
|
-
|
64
|
-
|
96
|
+
# Initialize vertices if provided
|
97
|
+
for vertex in options.get('initial_vertices', []):
|
98
|
+
self.add_vertex(vertex)
|
65
99
|
|
66
|
-
|
100
|
+
# Initialize edges if provided
|
101
|
+
for edge in options.get('initial_edges', []):
|
102
|
+
if len(edge) == 2:
|
103
|
+
self.add_edge(edge[0], edge[1])
|
104
|
+
elif len(edge) == 3:
|
105
|
+
self.add_edge(edge[0], edge[1], edge[2])
|
67
106
|
|
68
|
-
def
|
69
|
-
"""Get the
|
70
|
-
return
|
107
|
+
def get_supported_traits(self) -> NodeTrait:
|
108
|
+
"""Get the traits supported by the adjacency list strategy."""
|
109
|
+
return NodeTrait.GRAPH | NodeTrait.SPARSE | NodeTrait.FAST_NEIGHBORS
|
71
110
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
'nodes': self._nodes,
|
76
|
-
'edges': {node: neighbors for node, neighbors in self._adj_list.items() if neighbors}
|
77
|
-
}
|
111
|
+
# ============================================================================
|
112
|
+
# CORE GRAPH OPERATIONS (Industry Standard)
|
113
|
+
# ============================================================================
|
78
114
|
|
79
|
-
def
|
80
|
-
"""
|
81
|
-
|
82
|
-
edges = data.get('edges', {})
|
115
|
+
def add_vertex(self, vertex: str, data: Any = None) -> None:
|
116
|
+
"""
|
117
|
+
Add a vertex to the graph.
|
83
118
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
119
|
+
Time: O(1)
|
120
|
+
Space: O(1)
|
121
|
+
"""
|
122
|
+
if vertex not in self._nodes:
|
123
|
+
self._nodes[vertex] = data
|
124
|
+
if vertex not in self._adj_list:
|
125
|
+
self._adj_list[vertex] = []
|
126
|
+
|
127
|
+
def add_edge(self, from_vertex: str, to_vertex: str, weight: float = 1.0) -> None:
|
128
|
+
"""
|
129
|
+
Add an edge between two vertices.
|
130
|
+
|
131
|
+
Time: O(1) amortized
|
132
|
+
Space: O(1)
|
133
|
+
|
134
|
+
Note: Automatically creates vertices if they don't exist
|
135
|
+
"""
|
136
|
+
# Ensure vertices exist
|
137
|
+
self.add_vertex(from_vertex)
|
138
|
+
self.add_vertex(to_vertex)
|
139
|
+
|
140
|
+
# Check if edge already exists (avoid duplicates)
|
141
|
+
neighbors = self._adj_list[from_vertex]
|
97
142
|
for i, (neighbor, _) in enumerate(neighbors):
|
98
|
-
if neighbor ==
|
99
|
-
|
143
|
+
if neighbor == to_vertex:
|
144
|
+
# Update existing edge weight
|
145
|
+
neighbors[i] = (to_vertex, weight)
|
100
146
|
return
|
101
147
|
|
102
|
-
|
148
|
+
# Add new edge
|
149
|
+
neighbors.append((to_vertex, weight))
|
150
|
+
self._edge_count += 1
|
151
|
+
|
152
|
+
# For undirected graphs, add reverse edge
|
153
|
+
if not self._is_directed:
|
154
|
+
reverse_neighbors = self._adj_list[to_vertex]
|
155
|
+
# Check if reverse edge exists
|
156
|
+
found = False
|
157
|
+
for i, (neighbor, _) in enumerate(reverse_neighbors):
|
158
|
+
if neighbor == from_vertex:
|
159
|
+
reverse_neighbors[i] = (from_vertex, weight)
|
160
|
+
found = True
|
161
|
+
break
|
162
|
+
if not found:
|
163
|
+
reverse_neighbors.append((from_vertex, weight))
|
103
164
|
|
104
|
-
def remove_edge(self,
|
105
|
-
"""
|
106
|
-
|
165
|
+
def remove_edge(self, from_vertex: str, to_vertex: str) -> bool:
|
166
|
+
"""
|
167
|
+
Remove an edge between two vertices.
|
168
|
+
|
169
|
+
Time: O(degree(from_vertex))
|
170
|
+
|
171
|
+
Returns:
|
172
|
+
True if edge was removed, False if not found
|
173
|
+
"""
|
174
|
+
if from_vertex not in self._adj_list:
|
107
175
|
return False
|
108
176
|
|
109
|
-
neighbors = self._adj_list[
|
177
|
+
neighbors = self._adj_list[from_vertex]
|
110
178
|
for i, (neighbor, _) in enumerate(neighbors):
|
111
|
-
if neighbor ==
|
179
|
+
if neighbor == to_vertex:
|
112
180
|
neighbors.pop(i)
|
181
|
+
self._edge_count -= 1
|
182
|
+
|
183
|
+
# For undirected graphs, remove reverse edge
|
184
|
+
if not self._is_directed:
|
185
|
+
reverse_neighbors = self._adj_list.get(to_vertex, [])
|
186
|
+
for j, (n, _) in enumerate(reverse_neighbors):
|
187
|
+
if n == from_vertex:
|
188
|
+
reverse_neighbors.pop(j)
|
189
|
+
break
|
190
|
+
|
113
191
|
return True
|
192
|
+
|
114
193
|
return False
|
115
194
|
|
116
|
-
def
|
117
|
-
"""
|
118
|
-
|
195
|
+
def remove_vertex(self, vertex: str) -> bool:
|
196
|
+
"""
|
197
|
+
Remove a vertex and all its edges.
|
198
|
+
|
199
|
+
Time: O(V + E) worst case
|
200
|
+
|
201
|
+
Returns:
|
202
|
+
True if vertex was removed, False if not found
|
203
|
+
"""
|
204
|
+
if vertex not in self._nodes:
|
119
205
|
return False
|
120
206
|
|
121
|
-
|
122
|
-
|
207
|
+
# Remove vertex data
|
208
|
+
del self._nodes[vertex]
|
209
|
+
|
210
|
+
# Count edges to be removed
|
211
|
+
edges_removed = len(self._adj_list.get(vertex, []))
|
212
|
+
|
213
|
+
# Remove all edges TO this vertex
|
214
|
+
for node in list(self._adj_list.keys()):
|
215
|
+
if node == vertex:
|
216
|
+
continue
|
217
|
+
self._adj_list[node] = [
|
218
|
+
(neighbor, weight)
|
219
|
+
for neighbor, weight in self._adj_list[node]
|
220
|
+
if neighbor != vertex
|
221
|
+
]
|
222
|
+
|
223
|
+
# Remove vertex's adjacency list
|
224
|
+
if vertex in self._adj_list:
|
225
|
+
del self._adj_list[vertex]
|
226
|
+
|
227
|
+
self._edge_count -= edges_removed
|
228
|
+
|
229
|
+
return True
|
230
|
+
|
231
|
+
def get_neighbors(self, vertex: str) -> List[str]:
|
232
|
+
"""
|
233
|
+
Get all neighbors of a vertex.
|
234
|
+
|
235
|
+
Time: O(1) to O(degree)
|
236
|
+
|
237
|
+
Returns:
|
238
|
+
List of neighbor vertices
|
239
|
+
"""
|
240
|
+
if vertex not in self._adj_list:
|
241
|
+
return []
|
242
|
+
return [neighbor for neighbor, _ in self._adj_list[vertex]]
|
243
|
+
|
244
|
+
def get_neighbors_with_weights(self, vertex: str) -> List[Tuple[str, float]]:
|
245
|
+
"""
|
246
|
+
Get all neighbors with edge weights.
|
247
|
+
|
248
|
+
Time: O(degree)
|
249
|
+
|
250
|
+
Returns:
|
251
|
+
List of (neighbor, weight) tuples
|
252
|
+
"""
|
253
|
+
if vertex not in self._adj_list:
|
254
|
+
return []
|
255
|
+
return self._adj_list[vertex].copy()
|
256
|
+
|
257
|
+
def has_edge(self, from_vertex: str, to_vertex: str) -> bool:
|
258
|
+
"""
|
259
|
+
Check if an edge exists.
|
260
|
+
|
261
|
+
Time: O(degree(from_vertex))
|
262
|
+
"""
|
263
|
+
if from_vertex not in self._adj_list:
|
264
|
+
return False
|
265
|
+
|
266
|
+
for neighbor, _ in self._adj_list[from_vertex]:
|
267
|
+
if neighbor == to_vertex:
|
123
268
|
return True
|
124
269
|
return False
|
125
270
|
|
126
|
-
def get_edge_weight(self,
|
127
|
-
"""
|
128
|
-
|
271
|
+
def get_edge_weight(self, from_vertex: str, to_vertex: str) -> Optional[float]:
|
272
|
+
"""
|
273
|
+
Get the weight of an edge.
|
274
|
+
|
275
|
+
Time: O(degree(from_vertex))
|
276
|
+
|
277
|
+
Returns:
|
278
|
+
Edge weight or None if edge doesn't exist
|
279
|
+
"""
|
280
|
+
if from_vertex not in self._adj_list:
|
129
281
|
return None
|
130
282
|
|
131
|
-
for neighbor, weight in self._adj_list[
|
132
|
-
if neighbor ==
|
283
|
+
for neighbor, weight in self._adj_list[from_vertex]:
|
284
|
+
if neighbor == to_vertex:
|
133
285
|
return weight
|
286
|
+
|
134
287
|
return None
|
135
288
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
return []
|
140
|
-
return [neighbor for neighbor, _ in self._adj_list[node]]
|
289
|
+
# ============================================================================
|
290
|
+
# GRAPH ANALYSIS METHODS
|
291
|
+
# ============================================================================
|
141
292
|
|
142
|
-
def
|
143
|
-
"""Get
|
144
|
-
if
|
145
|
-
return
|
146
|
-
return self._adj_list[
|
293
|
+
def degree(self, vertex: str) -> int:
|
294
|
+
"""Get the degree of a vertex (out-degree for directed graphs)."""
|
295
|
+
if vertex not in self._adj_list:
|
296
|
+
return 0
|
297
|
+
return len(self._adj_list[vertex])
|
147
298
|
|
148
|
-
def
|
149
|
-
"""
|
299
|
+
def in_degree(self, vertex: str) -> int:
|
300
|
+
"""
|
301
|
+
Get the in-degree of a vertex.
|
302
|
+
|
303
|
+
Time: O(E) - must scan all edges
|
304
|
+
"""
|
305
|
+
if vertex not in self._nodes:
|
306
|
+
return 0
|
307
|
+
|
150
308
|
count = 0
|
151
309
|
for neighbors in self._adj_list.values():
|
152
310
|
for neighbor, _ in neighbors:
|
153
|
-
if neighbor ==
|
311
|
+
if neighbor == vertex:
|
154
312
|
count += 1
|
155
313
|
return count
|
156
314
|
|
157
|
-
def
|
158
|
-
"""Get the out-degree of a
|
159
|
-
|
160
|
-
return 0
|
161
|
-
return len(self._adj_list[node])
|
315
|
+
def out_degree(self, vertex: str) -> int:
|
316
|
+
"""Get the out-degree of a vertex (same as degree for directed graphs)."""
|
317
|
+
return self.degree(vertex)
|
162
318
|
|
163
|
-
|
164
|
-
|
165
|
-
|
319
|
+
# ============================================================================
|
320
|
+
# GRAPH TRAVERSAL ALGORITHMS (Production-Grade)
|
321
|
+
# ============================================================================
|
166
322
|
|
167
|
-
def
|
168
|
-
"""
|
169
|
-
|
170
|
-
|
323
|
+
def dfs(self, start: str, visit_fn: Optional[Callable[[str], None]] = None) -> List[str]:
|
324
|
+
"""
|
325
|
+
Depth-First Search from start vertex.
|
326
|
+
|
327
|
+
Time: O(V + E)
|
328
|
+
Space: O(V) for recursion stack
|
171
329
|
|
172
|
-
|
173
|
-
|
330
|
+
Args:
|
331
|
+
start: Starting vertex
|
332
|
+
visit_fn: Optional callback for each visited vertex
|
333
|
+
|
334
|
+
Returns:
|
335
|
+
List of visited vertices in DFS order
|
336
|
+
"""
|
337
|
+
if start not in self._nodes:
|
338
|
+
return []
|
174
339
|
|
175
340
|
visited = set()
|
176
|
-
|
341
|
+
result = []
|
177
342
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
343
|
+
def dfs_recursive(vertex: str) -> None:
|
344
|
+
visited.add(vertex)
|
345
|
+
result.append(vertex)
|
346
|
+
if visit_fn:
|
347
|
+
visit_fn(vertex)
|
182
348
|
|
183
|
-
|
184
|
-
|
185
|
-
|
349
|
+
for neighbor, _ in self._adj_list.get(vertex, []):
|
350
|
+
if neighbor not in visited:
|
351
|
+
dfs_recursive(neighbor)
|
352
|
+
|
353
|
+
dfs_recursive(start)
|
354
|
+
return result
|
355
|
+
|
356
|
+
def bfs(self, start: str, visit_fn: Optional[Callable[[str], None]] = None) -> List[str]:
|
357
|
+
"""
|
358
|
+
Breadth-First Search from start vertex.
|
359
|
+
|
360
|
+
Time: O(V + E)
|
361
|
+
Space: O(V) for queue
|
362
|
+
|
363
|
+
Args:
|
364
|
+
start: Starting vertex
|
365
|
+
visit_fn: Optional callback for each visited vertex
|
366
|
+
|
367
|
+
Returns:
|
368
|
+
List of visited vertices in BFS order
|
369
|
+
"""
|
370
|
+
if start not in self._nodes:
|
371
|
+
return []
|
372
|
+
|
373
|
+
visited = set([start])
|
374
|
+
result = [start]
|
375
|
+
queue = deque([start]) # Use deque for O(1) operations
|
376
|
+
|
377
|
+
if visit_fn:
|
378
|
+
visit_fn(start)
|
379
|
+
|
380
|
+
while queue:
|
381
|
+
vertex = queue.popleft() # O(1) with deque
|
186
382
|
|
187
|
-
for neighbor, _ in self._adj_list.get(
|
383
|
+
for neighbor, _ in self._adj_list.get(vertex, []):
|
188
384
|
if neighbor not in visited:
|
385
|
+
visited.add(neighbor)
|
386
|
+
result.append(neighbor)
|
189
387
|
queue.append(neighbor)
|
388
|
+
if visit_fn:
|
389
|
+
visit_fn(neighbor)
|
190
390
|
|
191
|
-
return
|
391
|
+
return result
|
392
|
+
|
393
|
+
def find_path(self, start: str, end: str) -> List[str]:
|
394
|
+
"""
|
395
|
+
Find path between two vertices using BFS.
|
396
|
+
|
397
|
+
Time: O(V + E)
|
398
|
+
Space: O(V)
|
399
|
+
|
400
|
+
Returns:
|
401
|
+
List representing path from start to end, or empty list if no path
|
402
|
+
"""
|
403
|
+
if start not in self._nodes or end not in self._nodes:
|
404
|
+
return []
|
405
|
+
|
406
|
+
if start == end:
|
407
|
+
return [start]
|
408
|
+
|
409
|
+
visited = set([start])
|
410
|
+
queue = deque([(start, [start])])
|
411
|
+
|
412
|
+
while queue:
|
413
|
+
vertex, path = queue.popleft()
|
414
|
+
|
415
|
+
for neighbor, _ in self._adj_list.get(vertex, []):
|
416
|
+
if neighbor == end:
|
417
|
+
return path + [neighbor]
|
418
|
+
|
419
|
+
if neighbor not in visited:
|
420
|
+
visited.add(neighbor)
|
421
|
+
queue.append((neighbor, path + [neighbor]))
|
422
|
+
|
423
|
+
return [] # No path found
|
424
|
+
|
425
|
+
def has_cycle(self) -> bool:
|
426
|
+
"""
|
427
|
+
Check if the graph contains a cycle.
|
428
|
+
|
429
|
+
Time: O(V + E)
|
430
|
+
Space: O(V)
|
431
|
+
|
432
|
+
Returns:
|
433
|
+
True if graph has a cycle, False otherwise
|
434
|
+
"""
|
435
|
+
if not self._is_directed:
|
436
|
+
# Undirected graph cycle detection
|
437
|
+
visited = set()
|
438
|
+
|
439
|
+
def has_cycle_undirected(vertex: str, parent: Optional[str]) -> bool:
|
440
|
+
visited.add(vertex)
|
441
|
+
|
442
|
+
for neighbor, _ in self._adj_list.get(vertex, []):
|
443
|
+
if neighbor not in visited:
|
444
|
+
if has_cycle_undirected(neighbor, vertex):
|
445
|
+
return True
|
446
|
+
elif neighbor != parent:
|
447
|
+
return True
|
448
|
+
|
449
|
+
return False
|
450
|
+
|
451
|
+
for vertex in self._nodes:
|
452
|
+
if vertex not in visited:
|
453
|
+
if has_cycle_undirected(vertex, None):
|
454
|
+
return True
|
455
|
+
|
456
|
+
return False
|
457
|
+
else:
|
458
|
+
# Directed graph cycle detection (using DFS with colors)
|
459
|
+
WHITE, GRAY, BLACK = 0, 1, 2
|
460
|
+
color = {v: WHITE for v in self._nodes}
|
461
|
+
|
462
|
+
def has_cycle_directed(vertex: str) -> bool:
|
463
|
+
color[vertex] = GRAY
|
464
|
+
|
465
|
+
for neighbor, _ in self._adj_list.get(vertex, []):
|
466
|
+
if color[neighbor] == GRAY: # Back edge found
|
467
|
+
return True
|
468
|
+
if color[neighbor] == WHITE:
|
469
|
+
if has_cycle_directed(neighbor):
|
470
|
+
return True
|
471
|
+
|
472
|
+
color[vertex] = BLACK
|
473
|
+
return False
|
474
|
+
|
475
|
+
for vertex in self._nodes:
|
476
|
+
if color[vertex] == WHITE:
|
477
|
+
if has_cycle_directed(vertex):
|
478
|
+
return True
|
479
|
+
|
480
|
+
return False
|
481
|
+
|
482
|
+
def topological_sort(self) -> Optional[List[str]]:
|
483
|
+
"""
|
484
|
+
Perform topological sort on the graph.
|
485
|
+
|
486
|
+
Time: O(V + E)
|
487
|
+
Space: O(V)
|
488
|
+
|
489
|
+
Returns:
|
490
|
+
List of vertices in topological order, or None if graph has cycle
|
491
|
+
|
492
|
+
Note: Only works on directed acyclic graphs (DAGs)
|
493
|
+
"""
|
494
|
+
if not self._is_directed:
|
495
|
+
return None # Topological sort only for directed graphs
|
496
|
+
|
497
|
+
if self.has_cycle():
|
498
|
+
return None # Cannot topologically sort cyclic graphs
|
499
|
+
|
500
|
+
visited = set()
|
501
|
+
stack = []
|
502
|
+
|
503
|
+
def dfs_topological(vertex: str) -> None:
|
504
|
+
visited.add(vertex)
|
505
|
+
|
506
|
+
for neighbor, _ in self._adj_list.get(vertex, []):
|
507
|
+
if neighbor not in visited:
|
508
|
+
dfs_topological(neighbor)
|
509
|
+
|
510
|
+
stack.append(vertex)
|
511
|
+
|
512
|
+
for vertex in self._nodes:
|
513
|
+
if vertex not in visited:
|
514
|
+
dfs_topological(vertex)
|
515
|
+
|
516
|
+
return list(reversed(stack))
|
192
517
|
|
193
518
|
def get_connected_components(self) -> List[Set[str]]:
|
194
|
-
"""
|
519
|
+
"""
|
520
|
+
Get all connected components in the graph.
|
521
|
+
|
522
|
+
Time: O(V + E)
|
523
|
+
Space: O(V)
|
524
|
+
|
525
|
+
Returns:
|
526
|
+
List of sets, each containing vertices in a connected component
|
527
|
+
"""
|
195
528
|
visited = set()
|
196
529
|
components = []
|
197
530
|
|
198
|
-
for
|
199
|
-
if
|
531
|
+
for start_vertex in self._nodes:
|
532
|
+
if start_vertex not in visited:
|
533
|
+
# BFS to find component
|
200
534
|
component = set()
|
201
|
-
queue = [
|
535
|
+
queue = deque([start_vertex])
|
536
|
+
visited.add(start_vertex)
|
202
537
|
|
203
538
|
while queue:
|
204
|
-
|
205
|
-
|
206
|
-
continue
|
207
|
-
|
208
|
-
visited.add(current)
|
209
|
-
component.add(current)
|
539
|
+
vertex = queue.popleft()
|
540
|
+
component.add(vertex)
|
210
541
|
|
211
|
-
|
212
|
-
for neighbor, _ in self._adj_list.get(current, []):
|
542
|
+
for neighbor, _ in self._adj_list.get(vertex, []):
|
213
543
|
if neighbor not in visited:
|
544
|
+
visited.add(neighbor)
|
214
545
|
queue.append(neighbor)
|
546
|
+
|
547
|
+
# For undirected graphs, also check incoming edges
|
548
|
+
if not self._is_directed:
|
549
|
+
for other_vertex in self._nodes:
|
550
|
+
if other_vertex not in visited:
|
551
|
+
for n, _ in self._adj_list.get(other_vertex, []):
|
552
|
+
if n == vertex:
|
553
|
+
visited.add(other_vertex)
|
554
|
+
queue.append(other_vertex)
|
555
|
+
break
|
215
556
|
|
216
|
-
|
217
|
-
components.append(component)
|
557
|
+
components.append(component)
|
218
558
|
|
219
559
|
return components
|
220
560
|
|
561
|
+
def is_connected(self, from_vertex: str, to_vertex: str) -> bool:
|
562
|
+
"""
|
563
|
+
Check if there's a path between two vertices.
|
564
|
+
|
565
|
+
Time: O(V + E)
|
566
|
+
|
567
|
+
Returns:
|
568
|
+
True if vertices are connected, False otherwise
|
569
|
+
"""
|
570
|
+
return len(self.find_path(from_vertex, to_vertex)) > 0
|
571
|
+
|
572
|
+
# ============================================================================
|
573
|
+
# REQUIRED ABSTRACT METHODS (from ANodeStrategy)
|
574
|
+
# ============================================================================
|
575
|
+
|
576
|
+
def put(self, key: Any, value: Any = None) -> None:
|
577
|
+
"""Store vertex with data."""
|
578
|
+
self.add_vertex(str(key), value)
|
579
|
+
|
580
|
+
def get(self, key: Any, default: Any = None) -> Any:
|
581
|
+
"""Get vertex data."""
|
582
|
+
return self._nodes.get(str(key), default)
|
583
|
+
|
584
|
+
def has(self, key: Any) -> bool:
|
585
|
+
"""Check if vertex exists."""
|
586
|
+
return str(key) in self._nodes
|
587
|
+
|
588
|
+
def delete(self, key: Any) -> bool:
|
589
|
+
"""Delete a vertex and all its edges."""
|
590
|
+
return self.remove_vertex(str(key))
|
591
|
+
|
592
|
+
def keys(self) -> Iterator[Any]:
|
593
|
+
"""Get all vertex IDs."""
|
594
|
+
return iter(self._nodes.keys())
|
595
|
+
|
596
|
+
def values(self) -> Iterator[Any]:
|
597
|
+
"""Get all vertex data."""
|
598
|
+
return iter(self._nodes.values())
|
599
|
+
|
600
|
+
def items(self) -> Iterator[tuple[Any, Any]]:
|
601
|
+
"""Get all vertices as (id, data) pairs."""
|
602
|
+
return iter(self._nodes.items())
|
603
|
+
|
604
|
+
# ============================================================================
|
605
|
+
# UTILITY METHODS
|
606
|
+
# ============================================================================
|
607
|
+
|
608
|
+
def size(self) -> int:
|
609
|
+
"""Get the number of vertices."""
|
610
|
+
return len(self._nodes)
|
611
|
+
|
612
|
+
def is_empty(self) -> bool:
|
613
|
+
"""Check if graph has no vertices."""
|
614
|
+
return len(self._nodes) == 0
|
615
|
+
|
221
616
|
def clear(self) -> None:
|
222
|
-
"""Clear all
|
617
|
+
"""Clear all vertices and edges."""
|
223
618
|
self._nodes.clear()
|
224
619
|
self._adj_list.clear()
|
620
|
+
self._edge_count = 0
|
225
621
|
|
226
|
-
def
|
227
|
-
"""
|
228
|
-
|
229
|
-
yield node
|
622
|
+
def vertex_count(self) -> int:
|
623
|
+
"""Get the number of vertices."""
|
624
|
+
return len(self._nodes)
|
230
625
|
|
231
|
-
def
|
232
|
-
"""
|
233
|
-
return
|
626
|
+
def edge_count(self) -> int:
|
627
|
+
"""Get the number of edges."""
|
628
|
+
return self._edge_count
|
234
629
|
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
630
|
+
def vertices(self) -> List[str]:
|
631
|
+
"""Get all vertices."""
|
632
|
+
return list(self._nodes.keys())
|
633
|
+
|
634
|
+
def edges(self) -> List[Tuple[str, str, float]]:
|
635
|
+
"""Get all edges as (from, to, weight) tuples."""
|
636
|
+
result = []
|
637
|
+
for from_vertex, neighbors in self._adj_list.items():
|
638
|
+
for to_vertex, weight in neighbors:
|
639
|
+
result.append((from_vertex, to_vertex, weight))
|
640
|
+
return result
|
641
|
+
|
642
|
+
def to_native(self) -> Dict[str, Any]:
|
643
|
+
"""Convert graph to native dictionary format."""
|
644
|
+
return {
|
645
|
+
'vertices': self._nodes,
|
646
|
+
'edges': {
|
647
|
+
vertex: [(neighbor, weight) for neighbor, weight in neighbors]
|
648
|
+
for vertex, neighbors in self._adj_list.items()
|
649
|
+
if neighbors
|
650
|
+
},
|
651
|
+
'is_directed': self._is_directed,
|
652
|
+
'vertex_count': self.vertex_count(),
|
653
|
+
'edge_count': self.edge_count()
|
654
|
+
}
|
655
|
+
|
656
|
+
def from_native(self, data: Dict[str, Any]) -> None:
|
657
|
+
"""Load graph from native dictionary format."""
|
658
|
+
self._nodes.clear()
|
659
|
+
self._adj_list.clear()
|
660
|
+
self._edge_count = 0
|
243
661
|
|
244
|
-
|
245
|
-
|
662
|
+
# Load vertices
|
663
|
+
for vertex, vertex_data in data.get('vertices', {}).items():
|
664
|
+
self._nodes[vertex] = vertex_data
|
246
665
|
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
if current in visited:
|
253
|
-
continue
|
254
|
-
visited.add(current)
|
255
|
-
|
256
|
-
for neighbor, _ in self._adj_list.get(current, []):
|
257
|
-
if neighbor not in visited:
|
258
|
-
queue.append((neighbor, path + [neighbor]))
|
259
|
-
|
260
|
-
return [] # No path found
|
666
|
+
# Load edges
|
667
|
+
self._is_directed = data.get('is_directed', True)
|
668
|
+
for vertex, neighbors in data.get('edges', {}).items():
|
669
|
+
for neighbor, weight in neighbors:
|
670
|
+
self.add_edge(vertex, neighbor, weight)
|
261
671
|
|
262
|
-
def as_union_find(self):
|
263
|
-
"""Provide Union-Find behavioral view."""
|
264
|
-
return self
|
265
672
|
|
266
|
-
|
267
|
-
|
268
|
-
|
673
|
+
# ============================================================================
|
674
|
+
# PYTHON SPECIAL METHODS
|
675
|
+
# ============================================================================
|
676
|
+
|
677
|
+
def __len__(self) -> int:
|
678
|
+
"""Return the number of vertices."""
|
679
|
+
return len(self._nodes)
|
680
|
+
|
681
|
+
def __bool__(self) -> bool:
|
682
|
+
"""Return True if graph has vertices."""
|
683
|
+
return bool(self._nodes)
|
684
|
+
|
685
|
+
def __contains__(self, vertex: str) -> bool:
|
686
|
+
"""Check if vertex exists in graph."""
|
687
|
+
return vertex in self._nodes
|
688
|
+
|
689
|
+
def __iter__(self) -> Iterator[str]:
|
690
|
+
"""Iterate through all vertices."""
|
691
|
+
return iter(self._nodes.keys())
|
269
692
|
|
270
|
-
def
|
271
|
-
"""
|
272
|
-
|
693
|
+
def __repr__(self) -> str:
|
694
|
+
"""Professional string representation."""
|
695
|
+
graph_type = "directed" if self._is_directed else "undirected"
|
696
|
+
return f"AdjacencyListStrategy(vertices={self.vertex_count()}, edges={self.edge_count()}, {graph_type})"
|
697
|
+
|
698
|
+
def __str__(self) -> str:
|
699
|
+
"""Human-readable string representation."""
|
700
|
+
return f"Graph[V={self.vertex_count()}, E={self.edge_count()}]"
|
701
|
+
|
702
|
+
# ============================================================================
|
703
|
+
# PERFORMANCE METADATA
|
704
|
+
# ============================================================================
|
705
|
+
|
706
|
+
@property
|
707
|
+
def backend_info(self) -> Dict[str, Any]:
|
708
|
+
"""Get backend implementation info."""
|
709
|
+
return {
|
710
|
+
'strategy': 'ADJACENCY_LIST',
|
711
|
+
'backend': 'dict of lists (defaultdict)',
|
712
|
+
'graph_type': 'directed' if self._is_directed else 'undirected',
|
713
|
+
'complexity': {
|
714
|
+
'add_vertex': 'O(1)',
|
715
|
+
'add_edge': 'O(1)',
|
716
|
+
'remove_vertex': 'O(V + E)',
|
717
|
+
'remove_edge': 'O(degree)',
|
718
|
+
'get_neighbors': 'O(1)',
|
719
|
+
'has_edge': 'O(degree)',
|
720
|
+
'dfs': 'O(V + E)',
|
721
|
+
'bfs': 'O(V + E)',
|
722
|
+
'space': 'O(V + E)'
|
723
|
+
},
|
724
|
+
'best_for': 'sparse graphs, dynamic graphs, neighbor queries',
|
725
|
+
'thread_safe': False
|
726
|
+
}
|
727
|
+
|
728
|
+
@property
|
729
|
+
def metrics(self) -> Dict[str, Any]:
|
730
|
+
"""Get performance metrics."""
|
731
|
+
avg_degree = self.edge_count() / self.vertex_count() if self.vertex_count() > 0 else 0
|
732
|
+
max_edges = self.vertex_count() * (self.vertex_count() - 1)
|
733
|
+
if not self._is_directed:
|
734
|
+
max_edges //= 2
|
735
|
+
|
736
|
+
sparsity = 1 - (self.edge_count() / max_edges) if max_edges > 0 else 1.0
|
737
|
+
|
738
|
+
return {
|
739
|
+
'vertices': self.vertex_count(),
|
740
|
+
'edges': self.edge_count(),
|
741
|
+
'avg_degree': f"{avg_degree:.2f}",
|
742
|
+
'sparsity': f"{sparsity:.2%}",
|
743
|
+
'is_directed': self._is_directed,
|
744
|
+
'memory_usage': f"{(self.vertex_count() * 8 + self.edge_count() * 16)} bytes (estimated)"
|
745
|
+
}
|