Pros/cons of defining a graph as nested node objects versus a dictionary?

Question:

I am practicing a couple algorithms (DFS, BFS). To set up the practice examples, I need to make a graph with vertices and edges. I have seen two approaches – defining an array of vertices and an array of edges, and then combining them into a "graph" using a dictionary, like so:

graph = {'A': ['B', 'E', 'C'],
        'B': ['A', 'D', 'E'],
        'C': ['A', 'F', 'G'],
        'D': ['B', 'E'],
        'E': ['A', 'B', 'D'],
        'F': ['C'],
        'G': ['C']}

But in a video series made by the author of "cracking the coding interview", their approach was to define a "node" object, which holds an ID, and a list of adjacent/child nodes (in Java):

public static class Node {
private int id;
LinkedList<Node> adjacent = new LinkedList<Node>(); // nodes children
private Node(int id) {
    this.id = id; //set nodes ID
    }
}

The pitfall I see of using the latter method, is making a custom function to add edges, as well has lacking an immediate overview of the structure of the entire graph; To make edges, you have to first retrieve the node object associated with the ID by first traversing to it or using a hashmap, and then by using its reference, adding the destination node to that source node:

private Node getNode(int id) {} //method to retrieve node from hashmap
public void addEdge(int source, int destination) {
    Node s = getNode(source);
    Node d = getNode(destination);
    s.adjacent.add(d);  
}

While in comparison using a simple dictionary, it is trivial to add new edges:

graph['A'].append('D')

By using a node object, adding a new connection to every child of a node is more verbose (imagine the Node class as a Python class which takes an ID and list of node-object children):

node1 = Node('A', [])
node2 = Node('B', [node1])
node3 = Node('C', [node1, node2])

new_node = Node('F', [])

for node in node3.adjacent:
    node.adjacent.append(new_node) # adds 'F' node to every child node of 'C'

while using dictionaries, if I want to add new_node to every connection/child of node3:

for node in graph['C']:
    graph[node].append('F')

What are the benefits in space and time complexity in building graphs using node objects versus dictionaries? Why would the author use node objects instead of a dictionary? My immediate intuition says that using objects would allow you make something much more complex (like each node representing a server, with an IP, mac address, cache, etc) while a dictionary is probably only useful for studying the structure of the graph. Is this correct?

Asked By: ring0-collections

||

Answers:

What are the benefits in space and time complexity in building graphs using node objects versus dictionaries

In terms of space, the complexity is the same for both. But in terms of time, each has its’ own advantages.

As you said, if you need to query for a specific node, the dictionary is better, with an O(logn) query. But if you need to transverse the graph, the node version is slightly better, since getting an adjecant node is an O(1) operation.

Finally, if you do opt to use the dictionary version and you predict you won’t have a small number of adjecant nodes, you can store edges in a set instead of an array, since they’re most likely not ordered and querying a node for the existance of an adjecant node becomes an O(logn) operation instead of O(n). The same applies to the nodes version, using a set instead of a linked list. Just make sure the extra overhead of the insertions makes it worthwhile.

My immediate intuition says that using objects would allow you make something much more complex (like each node representing a server, with an IP, mac address, cache, etc) while a dictionary is probably only useful for studying the structure of the graph. Is this correct?

No. With the dictionary, you can either have a separate dictionary that associates with node (key) to its’ value, or if the value is small enough, like an IPv4, and it’s unique, you can just use it as a key.

Answered By: Pedro Cavalheiro