Skip to content

Graph Node

A GraphNode provides a simple node-based representation for basic graph structures. Each node contains a value and maintains a list of neighboring nodes, making it ideal for simple graph operations and learning graph concepts.

Overview

GraphNode is a lightweight implementation for representing individual nodes in a graph. Unlike the full Graph implementations, it focuses on simple node-to-node relationships without advanced graph operations. This makes it ideal for basic graph structures, learning purposes, or as building blocks for custom graph implementations.

Features

  • Simple design - minimal overhead with just value and neighbors
  • Direct connections - neighbors stored as a simple list for easy access
  • Flexible values - can store any type of data as the node value
  • Basic operations - add and remove neighbors with automatic duplicate prevention
  • Memory efficient - no overhead from graph-wide data structures
  • Educational - perfect for learning graph concepts and algorithms
  • Building blocks - can be used to construct custom graph implementations
  • Type agnostic - works with any hashable value type

Time Complexities

Operation Time Complexity Description
Create Node O(1) Instantiate with value
Add Neighbor O(n) Check for duplicates in neighbor list
Remove Neighbor O(n) Search and remove from neighbor list
Check Neighbor O(n) Linear search through neighbor list
Get Neighbors O(1) Direct access to neighbor list
Get Value O(1) Direct access to node value

Space Complexity: O(n) where n is the number of neighbors

Basic Usage

Creating and Initializing

from algo.data_structs.graphs import GraphNode

# Create nodes with different value types
person_node = GraphNode("Alice")
number_node = GraphNode(42)
data_node = GraphNode({"id": 1, "name": "Server"})

print(f"Person node: {person_node}")     # GraphNode(Alice)
print(f"Value: {person_node.value}")     # Alice
print(f"Neighbors: {person_node.neighbors}")  # []

# Node values can be any type
boolean_node = GraphNode(True)
tuple_node = GraphNode((1, 2, 3))
list_node = GraphNode([1, 2, 3])

print(f"Boolean node value: {boolean_node.value}")  # True
print(f"Tuple node value: {tuple_node.value}")      # (1, 2, 3)

Understanding Node Relationships

# Nodes start with no connections
node_a = GraphNode('A')
node_b = GraphNode('B')

print(f"A's neighbors: {len(node_a.neighbors)}")  # 0
print(f"B's neighbors: {len(node_b.neighbors)}")  # 0

# Connections are directional by default
node_a.add_neighbor(node_b)
print(f"A's neighbors: {len(node_a.neighbors)}")  # 1
print(f"B's neighbors: {len(node_b.neighbors)}")  # 0

# For bidirectional connections, add both directions
node_b.add_neighbor(node_a)
print(f"A's neighbors: {len(node_a.neighbors)}")  # 1
print(f"B's neighbors: {len(node_b.neighbors)}")  # 1

Adding and Managing Neighbors

# Create a small network
alice = GraphNode("Alice")
bob = GraphNode("Bob")
carol = GraphNode("Carol")
david = GraphNode("David")

# Add neighbors (connections)
alice.add_neighbor(bob)
alice.add_neighbor(carol)
bob.add_neighbor(david)

print(f"Alice's friends: {len(alice.neighbors)}")  # 2
print(f"Bob's friends: {len(bob.neighbors)}")      # 1

# Adding same neighbor again doesn't create duplicate
alice.add_neighbor(bob)
print(f"Alice's friends after duplicate: {len(alice.neighbors)}")  # Still 2

# Access neighbor nodes
print("Alice's connections:")
for neighbor in alice.neighbors:
    print(f"  → {neighbor.value}")

# Check if a specific node is a neighbor
is_neighbor = bob in alice.neighbors
print(f"Bob is Alice's neighbor: {is_neighbor}")  # True

Removing Neighbors

# Remove connections
alice.add_neighbor(david)  # Add David as Alice's friend
print(f"Alice's friends: {len(alice.neighbors)}")  # 3

alice.remove_neighbor(david)
print(f"Alice's friends after removing David: {len(alice.neighbors)}")  # 2

# Removing non-existent neighbor is safe (no error)
alice.remove_neighbor(david)  # Safe to call again
print(f"Alice's friends after safe removal: {len(alice.neighbors)}")  # Still 2

# Check remaining connections
remaining_friends = [neighbor.value for neighbor in alice.neighbors]
print(f"Alice's remaining friends: {remaining_friends}")

Basic Node Operations

from algo.data_structs.graphs import GraphNode

# Create nodes with different value types
person_node = GraphNode("Alice")
number_node = GraphNode(42)
data_node = GraphNode({"id": 1, "name": "Server"})

print(f"Person node: {person_node}")  # GraphNode(Alice)
print(f"Value: {person_node.value}")  # Alice
print(f"Neighbors: {person_node.neighbors}")  # []

Building a Simple Network

# Create a small social network
alice = GraphNode("Alice")
bob = GraphNode("Bob")
carol = GraphNode("Carol")
david = GraphNode("David")

# Add friendships (bidirectional)
alice.add_neighbor(bob)
bob.add_neighbor(alice)

alice.add_neighbor(carol)
carol.add_neighbor(alice)

bob.add_neighbor(david)
david.add_neighbor(bob)

carol.add_neighbor(david)
david.add_neighbor(carol)

# Explore the network
print(f"Alice's friends: {[str(friend) for friend in alice.neighbors]}")
print(f"David's friends: {[str(friend) for friend in david.neighbors]}")

# Check mutual connections
mutual_friends = set(alice.neighbors) & set(david.neighbors)
print(f"Alice and David's mutual friends: {[str(friend) for friend in mutual_friends]}")

Tree-like Structure

# Build a simple organizational hierarchy
ceo = GraphNode("CEO")
cto = GraphNode("CTO")
cfo = GraphNode("CFO")
dev_manager = GraphNode("Dev Manager")
qa_manager = GraphNode("QA Manager")

# Build hierarchy (directed relationships)
ceo.add_neighbor(cto)
ceo.add_neighbor(cfo)
cto.add_neighbor(dev_manager)
cto.add_neighbor(qa_manager)

# Traverse the hierarchy
def print_hierarchy(node, level=0):
    indent = "  " * level
    print(f"{indent}{node.value}")
    for subordinate in node.neighbors:
        print_hierarchy(subordinate, level + 1)

print("Organizational Structure:")
print_hierarchy(ceo)

Node Manipulation

# Create a dynamic network
hub = GraphNode("Hub")
node1 = GraphNode("Node1")
node2 = GraphNode("Node2")
node3 = GraphNode("Node3")

# Add connections
hub.add_neighbor(node1)
hub.add_neighbor(node2)
hub.add_neighbor(node3)

print(f"Hub connections: {len(hub.neighbors)}")

# Adding same neighbor again doesn't create duplicate
hub.add_neighbor(node1)
print(f"Hub connections after duplicate add: {len(hub.neighbors)}")

# Remove a connection
hub.remove_neighbor(node2)
print(f"Hub connections after removal: {len(hub.neighbors)}")
print(f"Remaining connections: {[str(n) for n in hub.neighbors]}")

# Safe removal of non-existent neighbor
hub.remove_neighbor(node2)  # No error
print(f"Connections after safe removal: {len(hub.neighbors)}")

Custom Node Data

# Nodes with complex data
class Location:
    def __init__(self, name, lat, lon):
        self.name = name
        self.lat = lat
        self.lon = lon

    def __str__(self):
        return f"{self.name} ({self.lat}, {self.lon})"

# Create location nodes
new_york = GraphNode(Location("New York", 40.7128, -74.0060))
chicago = GraphNode(Location("Chicago", 41.8781, -87.6298))
la = GraphNode(Location("Los Angeles", 34.0522, -118.2437))

# Connect cities
new_york.add_neighbor(chicago)
chicago.add_neighbor(la)
new_york.add_neighbor(la)

# Access location data
for neighbor in new_york.neighbors:
    location = neighbor.value
    print(f"From {new_york.value.name} to {location.name}")
    print(f"  Coordinates: {location.lat}, {location.lon}")

Simple Graph Traversal

# Manual graph traversal using nodes
def dfs_nodes(start_node, visited=None):
    """Simple DFS traversal using GraphNodes."""
    if visited is None:
        visited = set()

    result = []
    stack = [start_node]

    while stack:
        node = stack.pop()
        if node not in visited:
            visited.add(node)
            result.append(node.value)

            # Add neighbors to stack
            for neighbor in reversed(node.neighbors):
                if neighbor not in visited:
                    stack.append(neighbor)

    return result

# Create a small graph
a = GraphNode('A')
b = GraphNode('B')
c = GraphNode('C')
d = GraphNode('D')

a.add_neighbor(b)
a.add_neighbor(c)
b.add_neighbor(d)
c.add_neighbor(d)

# Traverse the graph
traversal = dfs_nodes(a)
print(f"DFS traversal: {traversal}")

When to Use GraphNode

Ideal Use Cases

  • Simple graphs - when you need basic node-to-node relationships
  • Learning and education - when studying graph concepts or teaching algorithms
  • Prototyping - quick graph prototypes without full implementation overhead
  • Building blocks - as components for more complex graph structures
  • Tree structures - when working with hierarchical data
  • Small networks - when the graph size is small and manageable
  • Custom implementations - when building specialized graph algorithms

Consider Full Graph Implementations When

  • Complex operations - need algorithms like cycle detection, topological sort
  • Large graphs - working with many vertices and edges
  • Performance critical - need optimized traversal and analysis operations
  • Weighted edges - need edge weights or complex edge properties
  • Graph algorithms - using algorithms like Dijkstra, Floyd-Warshall
  • Memory efficiency - need optimized storage for large graphs

Limitations

No Built-in Algorithms

GraphNode doesn't provide:

  • Graph traversal algorithms (DFS, BFS)
  • Cycle detection
  • Connectivity checking
  • Shortest path algorithms
  • Topological sorting

No Edge Weights

  • Cannot associate weights or costs with connections
  • All relationships are treated equally
  • No support for weighted graph algorithms

No Graph-level Operations

  • No centralized vertex/edge management
  • No graph-wide statistics or analysis
  • Manual memory management of node relationships
  • No global graph properties (connectivity, etc.)

Performance Considerations

  • O(n) neighbor operations - list-based storage requires linear search
  • No optimization for large numbers of connections
  • Memory management - potential issues with circular references
  • No indexing - cannot quickly find nodes by value

Performance and Memory Tips

Efficient Neighbor Management

# For better performance with many neighbors, consider using sets
class OptimizedGraphNode:
    def __init__(self, value):
        self.value = value
        self._neighbors_set = set()  # For O(1) lookups
        self.neighbors = []  # Maintain list interface

    def add_neighbor(self, neighbor):
        if neighbor not in self._neighbors_set:
            self._neighbors_set.add(neighbor)
            self.neighbors.append(neighbor)

    def remove_neighbor(self, neighbor):
        if neighbor in self._neighbors_set:
            self._neighbors_set.remove(neighbor)
            self.neighbors.remove(neighbor)

    def has_neighbor(self, neighbor):
        return neighbor in self._neighbors_set  # O(1) instead of O(n)

Memory Management for Large Networks

# Use weak references to avoid circular reference issues
import weakref

class WeakGraphNode:
    def __init__(self, value):
        self.value = value
        self._neighbors = []  # Store weak references

    def add_neighbor(self, neighbor):
        weak_ref = weakref.ref(neighbor)
        if weak_ref not in self._neighbors:
            self._neighbors.append(weak_ref)

    @property
    def neighbors(self):
        # Return live neighbors (filter out dead references)
        return [ref() for ref in self._neighbors if ref() is not None]

Batch Operations

# When building large networks, batch operations for efficiency
def build_network_efficiently(nodes, connections):
    """Build network with batch operations."""
    node_map = {node.value: node for node in nodes}

    # Add all connections in batch
    for from_val, to_val in connections:
        from_node = node_map[from_val]
        to_node = node_map[to_val]
        from_node.add_neighbor(to_node)

    return nodes

# Usage
nodes = [GraphNode(f"Node{i}") for i in range(100)]
connections = [(f"Node{i}", f"Node{(i+1)%100}") for i in range(100)]
network = build_network_efficiently(nodes, connections)

Migration to Full Graph Implementations

When your needs outgrow GraphNode, you can migrate to full implementations:

# Convert GraphNode structure to AdjacencyListGraph
from algo.data_structs.graphs import AdjacencyListGraph

def convert_nodes_to_graph(start_node, directed=False):
    """Convert a GraphNode structure to AdjacencyListGraph."""
    graph = AdjacencyListGraph(directed=directed)
    visited = set()

    def traverse_and_add(node):
        if node in visited:
            return

        visited.add(node)
        graph.add_vertex(node.value)

        for neighbor in node.neighbors:
            graph.add_edge(node.value, neighbor.value)
            traverse_and_add(neighbor)

    traverse_and_add(start_node)
    return graph

# Example migration
node_a = GraphNode('A')
node_b = GraphNode('B')
node_c = GraphNode('C')
node_a.add_neighbor(node_b)
node_b.add_neighbor(node_c)

# Convert to full graph implementation
full_graph = convert_nodes_to_graph(node_a)
print(f"Converted graph vertices: {full_graph.get_vertices()}")
print(f"Converted graph edges: {full_graph.get_edges()}")

# Now you can use advanced operations
print(f"Is connected: {full_graph.is_connected()}")
print(f"BFS traversal: {full_graph.bfs('A')}")

Best Practices

Design Patterns

# 1. Use factory pattern for creating connected nodes
class NodeFactory:
    @staticmethod
    def create_chain(values):
        """Create a chain of connected nodes."""
        nodes = [GraphNode(val) for val in values]
        for i in range(len(nodes) - 1):
            nodes[i].add_neighbor(nodes[i + 1])
        return nodes

    @staticmethod
    def create_star(center_val, satellite_vals):
        """Create a star topology with center node."""
        center = GraphNode(center_val)
        satellites = [GraphNode(val) for val in satellite_vals]

        for satellite in satellites:
            center.add_neighbor(satellite)
            satellite.add_neighbor(center)  # Bidirectional

        return center, satellites

# Usage
chain = NodeFactory.create_chain(['A', 'B', 'C', 'D'])
center, satellites = NodeFactory.create_star('Hub', ['Node1', 'Node2', 'Node3'])

Error Handling

# Robust node operations with error handling
class SafeGraphNode(GraphNode):
    def safe_add_neighbor(self, neighbor):
        """Add neighbor with validation."""
        if not isinstance(neighbor, GraphNode):
            raise TypeError("Neighbor must be a GraphNode instance")

        if neighbor is self:
            raise ValueError("Cannot add self as neighbor")

        self.add_neighbor(neighbor)

    def safe_remove_neighbor(self, neighbor):
        """Remove neighbor with error handling."""
        try:
            self.remove_neighbor(neighbor)
            return True
        except ValueError:
            return False  # Neighbor not found

Testing and Validation

# Utility functions for testing node networks
def validate_network(nodes):
    """Validate network structure and relationships."""
    issues = []

    for node in nodes:
        # Check for self-loops
        if node in node.neighbors:
            issues.append(f"Self-loop detected in {node.value}")

        # Check for bidirectional consistency
        for neighbor in node.neighbors:
            if node not in neighbor.neighbors:
                issues.append(f"Unidirectional link: {node.value} -> {neighbor.value}")

    return issues

def network_statistics(nodes):
    """Calculate basic network statistics."""
    if not nodes:
        return {}

    total_connections = sum(len(node.neighbors) for node in nodes)
    max_connections = max(len(node.neighbors) for node in nodes)
    min_connections = min(len(node.neighbors) for node in nodes)
    avg_connections = total_connections / len(nodes)

    return {
        'total_nodes': len(nodes),
        'total_connections': total_connections // 2,  # Bidirectional
        'average_connections': avg_connections,
        'max_connections': max_connections,
        'min_connections': min_connections
    }

# Example usage
nodes = [node_a, node_b, node_c]
issues = validate_network(nodes)
stats = network_statistics(nodes)

print(f"Network issues: {issues}")
print(f"Network stats: {stats}")

API Reference

Graph data structure implementations.

This module provides core graph components like GraphNode and imports from specialized graph implementation modules.

GraphNode(value)

A node in a graph.

add_neighbor(neighbor)

Add a neighbor to this node.

remove_neighbor(neighbor)

Remove a neighbor from this node.